<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[biny 的博客]]></title><description><![CDATA[记录学习过程与技术笔记。]]></description><link>https://binzi.top</link><image><url>https://binzi.top/img/avatar.jpg?v=biny-icon-1</url><title>biny 的博客</title><link>https://binzi.top</link></image><generator>Shiro (https://github.com/Innei/Shiro)</generator><lastBuildDate>Thu, 15 Oct 2026 11:35:54 GMT</lastBuildDate><atom:link href="https://binzi.top/feed" rel="self" type="application/rss+xml"/><pubDate>Thu, 15 Oct 2026 11:35:53 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[我是怎么设计 Biny Agent 的记忆系统]]></title><description><![CDATA[<link rel="preload" as="image" href="https://binzi.top/images/biny-memory/arch-overview.png"/><link rel="preload" as="image" href="https://binzi.top/images/biny-memory/short-term-layers.svg"/><link rel="preload" as="image" href="https://binzi.top/images/biny-memory/fact-lifecycle.png"/><link rel="preload" as="image" href="https://binzi.top/images/biny-memory/fact-example.png"/><link rel="preload" as="image" href="https://binzi.top/images/biny-memory/fact-recall.svg"/><link rel="preload" as="image" href="https://binzi.top/images/biny-memory/recall-defaults.png"/><link rel="preload" as="image" href="https://binzi.top/images/biny-memory/sleep-phases.svg"/><link rel="preload" as="image" href="https://binzi.top/images/biny-memory/sleep-consistency.svg"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/biny-agent-memory-system">https://binzi.top/posts/ai/biny-agent-memory-system</a></blockquote><div><p>我一直想要一个能持续理解同一个人的 Agent。</p><p>ChatGPT 上线记忆的时候，我喜欢那个效果：不用每次重新自我介绍，它记得你惯用的技术栈、说话习惯和正在做的事。产品本身是黑盒。我答不上来它记住了哪一条、为什么留、什么时候会忘。做 Biny 时我把记忆做成可以打开、可以逐条改的数据，每条都能查出从哪来。</p><p>动手前我看了 Codex 的后台两阶段归并、Claude Code 的项目笔记、OpenClaw 的文件人格加 Dreaming，还有 mem0 的条目化事实。基本单位用的是 mem0 那种：把还能复用的事实抽成独立条目，再放到本地，按层存放，批量维护。</p><p>下面按一条信息从哪来、存到哪、什么时候被取出来、怎么被清掉来写。</p>
<h2 id="">整体架构</h2><p>长期要留的东西不止事实。当前任务、给人读的长文本、反复出现的对象、电脑上实际做过的事，活多久、证据从哪来，都不一样。我把它们拆开存，各管各的整理，拼 prompt 的时候再放到一起。</p><p>回答的时候只按相关性捞几条事实。全库去重、过期、语义合并不放在这一轮，交给 Sleep，默认本机 03:00 之后、runtime 空闲时跑。</p><table><thead><tr><th> 层 </th><th> 记什么 </th><th> 怎么被用到 </th></tr></thead><tbody><tr><td> 短期工作记忆 </td><td> 当前做到哪、还剩什么 </td><td> 会话历史与压缩、Thread Brief、项目卡、Todo 与执行状态 </td></tr><tr><td> 长期事实 </td><td> 偏好、约束、稳定背景 </td><td> SQLite 事实条目；本地向量做语义 Top-K；Sleep 在后台巩固 </td></tr><tr><td> 时间证据 </td><td> 原话何时说出、相对日期指哪一天 </td><td> 挂在事实的来源锚点上，同文不同来源不直接合并 </td></tr><tr><td> 文件记忆 </td><td> 画像、日记、人格 </td><td> <code>USER.md</code>、<code>MEMORY.md</code>、近两天日记、<code>SOUL.md</code> 按约定注入，不做 Top-K </td></tr><tr><td> Crystal </td><td> 反复出现的项目和概念 </td><td> 累计提及与分布，过门槛后再整理成带材料的对象 </td></tr><tr><td> Activity </td><td> 电脑上实际做过的事 </td><td> 本地事件与 OCR；整理成工作记录，只有少数候选再进事实库 </td></tr></tbody></table><p>事实是主数据，embedding 是可以重建的索引。聊天历史另有全文检索，和事实召回各走各的，不做混合排序。</p><p><img src="https://binzi.top/images/biny-memory/arch-overview.png" alt="Biny 记忆系统总览：用户消息与电脑活动进入 Memory Runtime，短期工作记忆、长期事实、文件记忆与 Activity 在上下文装配处汇合；时间证据挂在事实上，Crystal 挂在长期知识旁边" height="0" width="0"/></p><p><em>图 1：几条主路径在拼 prompt 时汇合。时间证据写在事实上，Crystal 单独放。</em></p><h2 id="">短期工作记忆：让当前任务能接着做</h2><p>短期工作记忆管的是任务能不能接着做。你说“检查一下这个项目的登录接口”，Agent 查完，你接着说“把刚才发现的问题修复一下”。第二句没有路径，也没有细节，只能靠当前会话才知道指什么。</p><p>这里有两件不同的事：</p><ul><li><strong>上下文还在</strong>：对话很长，或者已经压缩过，模型仍知道当前在做什么。</li><li><strong>执行还能续</strong>：任务中断、进程退出，或者换了一个执行者，还能从断点接着做。</li></ul><p>模型记得下一步，不表示系统能安全地把工具调用续上。</p><p>我用了四层来配合这件事，粒度依次变粗：单会话内、会话级、项目级、任务级。</p><p><img src="https://binzi.top/images/biny-memory/short-term-layers.svg" alt="短期工作记忆的四层：会话历史与压缩、Thread Brief、项目卡、Todo 与执行状态"/></p><p><em>图 2：四层管的粒度不同，换一层顶不了另一层。</em></p><h3 id="--">第一层：会话历史 + 压缩</h3><p>同一会话能持续工作，我靠这一层。</p><p>我把消息按树存在会话日志里。每条我记下编号、上一条是谁，以及它属于回答里的哪一个位置。同一个位置可以有多个版本。恢复时我从当前这条分支拼上下文，不把文件里的全部文本按时间接起来。你改了问题、重试了回答，旧版本还能切回去，但我不拿它参与新分支的推理。</p><p>上下文快到模型上限时，我把较早的对话换成一份结构化摘要，最近一段原始消息留着。</p><p>关键在于这份摘要不是“我们刚才聊了什么”，它有固定的字段：</p><table><thead><tr><th> 字段 </th><th> 记录什么 </th></tr></thead><tbody><tr><td> Goal </td><td> 当前目标 </td></tr><tr><td> Constraints &amp; Preferences </td><td> 约束和偏好 </td></tr><tr><td> Done </td><td> 已完成 </td></tr><tr><td> In Progress </td><td> 正在进行 </td></tr><tr><td> Blocked </td><td> 被卡住的部分 </td></tr><tr><td> Key Decisions </td><td> 已做的取舍 </td></tr><tr><td> Errors &amp; Fixes </td><td> 遇到的错误和修法 </td></tr><tr><td> Next Steps </td><td> 下一步 </td></tr></tbody></table><p>最重要的是后面几项。假设压缩前正在修一个超时问题，摘要里应该长成这样：</p><table><thead><tr><th> 栏 </th><th> 写什么 </th></tr></thead><tbody><tr><td> 目标 </td><td> 修复工具调用超时 </td></tr><tr><td> 已完成 </td><td> 定位超时处理代码，新增失败测试，改完超时取消逻辑 </td></tr><tr><td> 进行中 </td><td> 单元测试已通过，集成测试还没跑 </td></tr><tr><td> 下一步 </td><td> 跑集成测试，检查是否引入了重复工具调用 </td></tr></tbody></table><p>压缩之后，模型不一定还能看到最早的完整对话，但它知道做到哪、还剩什么、下一步是什么。一份“研究了登录问题，进展顺利”读起来更短，可它指导不了后面的工作。</p><p>已经有摘要时，我把旧摘要和新对话合在一起，不从零再写一份。</p><p>触发线我不写成死的 80%。我先看显式保留量，再看触发百分比，最后才退到模型参考线或动态保留量。压完没有省下 token，我不发布新的检查点。摘要失败，我留着原历史，并进入冷却。</p><p>检查点我不放进正式历史，也不提升成系统指令。模型自己写的摘要，不能变成你的新授权。</p><p>摘要可能漏细节，文件也可能在这期间被改过。这一层续不上工具执行。</p><h3 id="thread-brief">第二层：Thread Brief，会话级工作摘要</h3><p>压缩是我给当前这次对话腾上下文。Thread Brief 是我事后整理这条会话，记下它主要在做什么。</p><p>我默认等这轮完成后再放进队列，不在回答的时候做。门槛是：至少 2 个你的轮次；队列最多积 64 个，我一个一个做；同一条会话已经在排队或正在整理，我不再排一次；内容没怎么变，新增不足 400 字，我沿用旧摘要。我不会每轮都调模型。</p><p>送进去之前我截断：你的一条最多 1200 字，助手 400 字，总共 12000 字，超出就留后面的。小模型按我规定的结构写：</p><pre class="language-json lang-json"><code class="language-json lang-json">{
  &quot;topic&quot;: &quot;Agent 工具系统&quot;,
  &quot;goal&quot;: &quot;修复工具调用超时与重复执行&quot;,
  &quot;objects&quot;: [&quot;ToolExecutionCoordinator&quot;, &quot;ToolSearch&quot;],
  &quot;conclusions&quot;: [&quot;超时取消逻辑需要统一&quot;],
  &quot;followUp&quot;: { &quot;what&quot;: &quot;补充集成测试&quot;, &quot;quote&quot;: &quot;用户要求完成后执行完整验收&quot; }
}
</code></pre>
<p>后续行动里符合条件的，我转成待办，并带来源原话。隔一天再开一条会话，我靠这个看出这条线做到哪。</p><h3 id="">第三层：项目卡，跨会话的工作记忆</h3><p>今天关掉会话，明天新开一条还想接着做，我靠这一层。</p><p>每个工作区我存一份项目卡：</p><pre class=""><code class="">brief         这个项目是什么
focus         当前正在处理什么
threads       相关的会话列表
</code></pre>
<p>长期背景我写这个项目是什么，当前焦点我写这一阶段在推什么。同一个项目下新开一条会话，我把已经确认过的背景和焦点放进去。</p><p>模型不能直接改已确认的项目卡。它可以提议，更新要你点头。聊几句就把项目目标改掉，这个我不要。</p><h3 id="todo-">第四层：Todo 与执行状态，真正记录“还剩什么”</h3><p>前面三层是让模型知道当前在做什么。真要接着执行，还得有写到磁盘上的任务状态。</p><p>待办是其中最小的一份。我存一份带状态的清单，每条有内容和状态，最多 50 条、每条 500 字，同时只允许一条标成进行中。每次我整份替换，不按序号改一条。这样不会出现「第 3 条到底是哪个」。</p><p>有内容时我把它拼进提示，并要求：开始做的标成进行中，真正做完才标完成。还有没做完的，不能说工作已经结束。</p><p>比待办更重的是执行断点。我把这一步的消息、系统提示、步骤数、预算和高水位写进 TurnStore，用临时文件写完再改名，并同步目录。恢复时我读的是这份状态，不是一段摘要。</p><p>中断之后我先核对高水位，再从日志和外部调用记录里补齐已经有结果的步骤，不重放动作。已经提交的终态，我优先于旧断点。结果未知、缺必要输入，或者需要新授权时，我停住，返回阻塞。</p><p>一次外部写入发出去了但没收到回执，远端可能已经做完。这时重试会做两次。我记下「未知」，停下来查，不把它当成失败。</p><h3 id="">三种“接着做”，靠的不是同一套东西</h3><table><thead><tr><th> 场景 </th><th> 主要机制 </th><th> 恢复粒度 </th></tr></thead><tbody><tr><td> 同一会话聊得太长 </td><td> 压缩 checkpoint </td><td> 对话上下文 </td></tr><tr><td> 换个会话继续同一个项目 </td><td> 项目卡 + 会话整理 </td><td> 项目目标和工作焦点 </td></tr><tr><td> 任务中断或换执行者 </td><td> Todo + TurnStore + 执行记录 </td><td> 任务进度和操作证据 </td></tr></tbody></table><p>会话整理我用来看这条线在做什么。它不是执行检查点。</p><p>压缩让模型知道做到哪。执行状态决定能不能安全接着做。这两件我分开建。</p><h2 id="">长期事实记忆：不是聊过就必须存</h2><p>这是我花时间最多的一层。整段对话不进数据库。</p><p>一条事实我这样走：</p><ol start="1"><li>一轮生成结束后，我只取结尾几条你和助手的话，不把整个会话送去提取。</li><li>我让小模型看里面有没有以后还能用的稳定事实。没有它就返回不记。一轮我期望 0～2 条。你明确要求忘掉或纠正时，它可以给出删除。</li><li>要留的我写成一条独立事实，放进 SQLite。正文是主数据。</li><li>我用本地模型给这条正文算一个向量，写进同一个库，下次按相似度找。</li><li>你下一轮再问，我先把问句改写成检索说法，再算向量，按相似度取几条，放进这一轮。</li><li>每天凌晨我用 Sleep 扫整张事实表，做去重、过期和合并。收掉的放进归档，不直接抹掉。</li></ol><p>提取是我在产生新事实。Sleep 是我在整理已经入库的事实。两件事我不放在同一轮。</p><p>我收的是偏好、长期约束、稳定背景和还在做的项目。寒暄、临时问题和一次性任务我丢掉。</p><p>我看的是这条信息以后还用不用得上。稳定的身份、偏好、长期目标和做事习惯我可以记。当前任务的细节、一次性请求、只在这一轮有用的话，我丢掉。你明确说忘掉、纠正，或者某条已经不成立，我可以删，不只是一直往上加。</p><p><img src="https://binzi.top/images/biny-memory/fact-lifecycle.png" alt="事实的记忆生命周期：提取门控分流长期价值与临时信息，长期价值进入小模型抽取与 SQLite，随后分别流向向量召回与 Sleep 巩固两条路径"/></p><p><em>图 3：写入之后分成两路，一路给下一轮召回，一路等 Sleep 整理。</em></p><h3 id="">一条事实长什么样</h3><p>存储形态是 <code>content</code> 加一段 metadata JSON，不是知识图谱。</p><table><thead><tr><th> 字段组 </th><th> 关键字段 </th><th> 作用 </th></tr></thead><tbody><tr><td> 内容 </td><td> <code>id</code>、<code>content</code> </td><td> 稳定标识与正文，正文最多 2,000 字符 </td></tr><tr><td> 分类 </td><td> <code>source</code>、<code>tags</code>、<code>rationale</code>、<code>importance</code> </td><td> 来源、组织方式、保存原因、重要度 </td></tr><tr><td> 生命周期 </td><td> <code>durability</code>、<code>expiresAt</code> </td><td> 区分临时事实和长期事实，表达显式到期 </td></tr><tr><td> 来源关联 </td><td> <code>threadId</code>、<code>messageId</code>、<code>userId</code> </td><td> 关联会话、消息、用户空间 </td></tr><tr><td> 原始时间证据 </td><td> <code>originAnchors</code> </td><td> 原消息 ID、发送时间、发送时区 </td></tr><tr><td> 版本 </td><td> <code>revision</code>、<code>createdAt</code>、<code>updatedAt</code> </td><td> 支撑异步工作后的版本核对 </td></tr><tr><td> 使用统计 </td><td> <code>accessCount</code>、<code>lastAccessedAt</code> </td><td> 参与后续维护判断 </td></tr><tr><td> 归档关系 </td><td> <code>originalId</code>、<code>archivedReason</code>、<code>mergedInto</code> </td><td> 解释何时、为何、被谁归档，合并去了哪 </td></tr></tbody></table><p>重要程度默认 0.5，标签最多 12 个，来源锚点只取输入的前 32 个再去重。正文有长度上限，长材料不往这里塞。</p><p>从原话到一条事实，中间大概是这样：</p><p><img src="https://binzi.top/images/biny-memory/fact-example.png" alt="示例：用户说「以后给我写技术博客，尽量简洁一点，不要写太多模板化的开头」，LLM 提取出候选事实「用户偏好简洁、非模板化的技术博客表达」，再存成带 id、content、importance、durability、tags、source、revision 的 JSON"/></p><p><em>图 4：原话不原样存。存进去的是一条可以单独检索、单独改的事实。</em></p><h3 id="">来源时间和保存时间必须分开</h3><p>这一点我故意拆开了。</p><p>你在周一说“下周五发布”，两周后 Sleep 才整理这条记忆。这时有三个时间：</p><ul><li>保存时间：这条记忆什么时候写进库；</li><li>原话说出的时间：挂在来源锚点上；</li><li>当时的时区：用来解释“明天”“下周五”。</li></ul><p>Sleep 跑的那天，不能拿来重算旧消息里的“明天”。精确去重分组时，来源锚点也算一条判据：正文一样、锚点不同，不合并。两周前那个“周五”和两周后那个“周五”字面相同，不是同一件事。</p><p>时间不单独做向量。它写在事实的 metadata 里，召回时跟着事实走，去重时用来挡住这种合并。</p><h3 id="">写入前先做语义判重</h3><p>自动新增时我先做这些：</p><ol start="1"><li>清洗候选正文和附带信息；</li><li>在同一个用户空间里找最相近的 5 条，最低相似度 0.3；</li><li>让模型判断它是同一事实、已有事实的子集，还是更含糊的重复说法；</li><li>重复我就跳过，有实质新增才写入。</li></ol><p>这不是严格去重。模型判断失败时我可能继续新增。新内容更丰富，我也不一定会改旧的那条，可能直接再写一条。手动新增我不做精确去重，每次合法新增都是一条新记录，同样的正文可以并存，留给后面的 Sleep。</p><h2 id="">召回：一条事实怎样回到当前回合</h2><p>召回我放在每一轮真正生成之前，不扫全库：</p><ol start="1"><li>我把你的问句交给小模型，改写成面向已存事实的检索说法。超时或失败，我用原句。</li><li>我用本地模型把这句算成向量。</li><li>我在向量表里按余弦相似度排序，取最靠前的几条，默认 5 条，低于 0.1 的不要。你问「你还记得我是做什么的吗」，我改写之后更接近 <code>User works as occupation job</code>，再拿这句去比库里的「User is a backend developer who prefers Go over Python」。</li><li>我按编号再读一遍事实表。已经删掉、已经归档，或者正文在等待期间被改过的，我丢掉。</li><li>剩下的我按字符和这一轮的 token 预算裁剪，整条放在当前问题前面当参考。写回聊天历史时我再剥掉，免得下一轮把它们当成新经历再学一遍。</li></ol><p>命中之后我记下这条被用过。这个计数我留给后面的维护，不拿来排这次的序。这次只看相似度。聊天记录我另有全文检索，那是查原话，不和事实召回混成一次排序。</p><p>召回比写入整齐。</p><p><img src="https://binzi.top/images/biny-memory/fact-recall.svg" alt="召回链路：查询改写、生成 Query Embedding、向量 Top-K、范围与版本复核、预算裁剪、注入推理"/></p><p><em>图 5：先按相似度排名，再过范围、版本和预算。</em></p><p>查询改写我默认开着，单独用一个小模型：最长 3 秒，输出很短，不做推理。改写结果我让它偏向英文检索说法，标识符、路径和技术词原样留下。失败我就用原来的问句。</p><p>我做这步，是因为「你还记得我平时怎么写文章吗」和「用户偏好的写作风格」在向量空间里差得很远。改写把前者往后者推。</p><p>向量我默认在本地算：<code>multilingual-e5-small</code>，384 维，Transformers.js + ONNX，另有 MiniLM 可选。搜索不打远端的向量接口，断网也能查。</p><p>默认的自动召回参数是这三个：Top-K 记忆条数 5、相似度门槛 0.1、Embedding 维度 384。</p><p><img src="https://binzi.top/images/biny-memory/recall-defaults.png" alt="默认自动召回参数：Top-K 记忆条数 5、相似度门槛 0.1、Embedding 维度 384"/></p><p>匹配就是一次 cosine 排序：</p><pre class="language-sql lang-sql"><code class="language-sql lang-sql">SELECT
  memory_id,
  1 - vec_distance_cosine(embedding, ?) AS score
FROM memory_embeddings
WHERE score &gt;= ?
ORDER BY score DESC
LIMIT ?;
</code></pre>
<h3 id="-cosine">同一套 cosine，门槛不一样</h3><p>下面只列写入和召回。Sleep 的 0.75 和 0.95 在后面。</p><table><thead><tr><th> 使用场景 </th><th> 候选数量 </th><th> 门槛 </th></tr></thead><tbody><tr><td> 自动事实召回 </td><td> 5 条（可配 1–20） </td><td> 0.1 </td></tr><tr><td> 显式 recall 工具 </td><td> 5 条 </td><td> 0.3 </td></tr><tr><td> 自动新增判重 </td><td> 5 条 </td><td> 0.3，再让模型判断 </td></tr><tr><td> 按描述删除事实 </td><td> 10 条 </td><td> 0，再让模型精确选择 </td></tr><tr><td> 回合后清理临时事实 </td><td> 20 条 </td><td> 0.3，再让模型选择 </td></tr></tbody></table><p>这些是相似度门槛。换一个 embedding 模型，同一个数的含义就变了。它们不是“这条事实有多正确”。</p><h3 id="">检索完还要再确认一次</h3><p>查询改写和生成 embedding 都是异步等待，这期间你可能刚好编辑或删掉了那条事实。所以候选回来之后，我会按 ID 重新读一遍选中的活动事实：</p><ul><li>已经删除 → 丢弃；</li><li>已经归档 → 丢弃；</li><li>版本号变了 → 丢弃；</li><li>只有访问统计变了 → 保留。</li></ul><p>防的是检索开始时还在、返回时已经过期的事实被送进模型。丢掉的名额不补。</p><p>预算有两层。事实召回自己有 12,000 字符上限，还要再服从这一轮的 token 预算。放不下就停，一条事实不切成半条。注入的内容放在当前用户消息旁边当参考，写回历史时剥掉，免得下一轮把旧记忆再学一遍。</p><h2 id="">向量可以重建</h2><p>事实是主数据。embedding 丢了可以重算。</p><p>向量我用 sqlite-vec 存在同一个库里，只针对事实正文，不把全部附带信息算进去。我用几层校验守住它：</p><ul><li>模型指纹和维度必须匹配；</li><li>向量的版本必须和活动事实的版本号对应；</li><li>异步算完回来，写入前我再查一次事实版本；</li><li>更新事实时我先去掉旧向量，再算新的；</li><li>重建我按 64 条分批，做完再看整张事实表变没变；</li><li>最后替换向量索引时，我在事务里再对一遍条数和版本。</li></ul><p>所以很晚才回来的旧向量盖不掉新事实，删掉的事实也不会因为旧向量又出现。</p><p>事实写成功、索引失败时，我不回滚事实，它只是暂时搜不到。召回我返回空，并带上降级原因。文档里写过可以退回文本检索，调用链没接上，我不当它已经能用。</p><h2 id="sleep">Sleep：为什么整理要放到后台</h2><p>Sleep 我不拿来看当天的聊天。我只扫已经在事实表里的条目。默认每天本地时间凌晨 3 点。那时候机器关着，下次打开我补跑当天这一次，不把缺的每一天都重放一遍。</p><p>一轮我按这个顺序做：</p><ol start="1"><li>正文规范化之后完全一样的，我留一条，其余归档。</li><li>临时事实我按到期和有没有被用过清理。长期事实不因为放得久就删。</li><li>我用已有向量把相近的事实收成簇。特别像的直接并。剩下的交给小模型决定删、留，还是合成一条新的。</li><li>归档超过保留期的，我再物理删除。</li></ol><p>写入时我已经判过一次重。跨记忆的去重、合并还要扫很多事实，反复调模型。塞进回答这一轮，你会等，也和你正在做的事抢机器。</p><p>所以提取和批量维护我分开。前台我只做提取和判重。Sleep 我放到本机 03:00 之后、runtime 空闲时跑。你开始干活，我就让开。</p><p>我只动已经入库的事实。不重放当天的聊天，不分析截图，不改用户资料，也不训练。</p><p><img src="https://binzi.top/images/biny-memory/sleep-phases.svg" alt="Sleep 四个阶段：精确去重、临时事实过期、相似整理、归档清理"/></p><p><em>图 6：四个阶段按顺序跑。前一个出错，后面停，免得整理失败了还把恢复来源删掉。</em></p><h3 id="">第一阶段：精确去重</h3><p>比较前做 Unicode NFC、合并连续空白、去掉首尾空白。大小写和标点的差别留着。分组还带上用户空间和来源消息锚点。</p><p>插入的时候不做这些。库里的正文就是提取模型给出的那句。比如连续两轮得到几乎同一句，只是第二句末尾多了一个空格，活动表里是两行：</p><table><thead><tr><th> 编号 </th><th> 正文 </th></tr></thead><tbody><tr><td> m-1 </td><td> User prefers TypeScript and pnpm. </td></tr><tr><td> m-2 </td><td> User prefers TypeScript and pnpm. </td></tr></tbody></table><p>第二行末尾多了一个空格。</p><p>Sleep 比的时候把首尾空白去掉，两句算同一句。同一个用户、同一个来源，就留被用得更多的 m-1，m-2 挪进归档。m-1 的正文不改。</p><table><thead><tr><th> 去向 </th><th> 编号 </th><th> 正文 </th><th> 说明 </th></tr></thead><tbody><tr><td> 活动表 </td><td> m-1 </td><td> User prefers TypeScript and pnpm. </td><td> 留下 </td></tr><tr><td> 归档 </td><td> m-2 </td><td> User prefers TypeScript and pnpm. </td><td> 精确重复，并进 m-1 </td></tr></tbody></table><p>如果一个是 <code>TypeScript</code>，另一个是 <code>typescript</code>，大小写不同，这一阶段两条都留着。</p><p>有两处特别的保护：</p><ul><li><strong>来源锚点不同的同文事实，不直接精确合并</strong>；</li><li><strong>没有来源锚点的临时事实，用自身 ID 区分</strong>，不按同文合并。</li></ul><p>分组后留一条，其余归档，原因记成精确重复，并记下并进了哪一条。</p><h3 id="">第二阶段：临时事实过期</h3><p>临时事实的默认保存期限是 30 天，但实际判断看的是另一组条件：</p><table><thead><tr><th> 条件 </th><th> 结果 </th></tr></thead><tbody><tr><td> 不是临时事实 </td><td> 不受这条规则影响 </td></tr><tr><td> 写明的到期时间已经过了 </td><td> 过期，即使被访问过 </td></tr><tr><td> 没有显式到期，但 <code>accessCount != 0</code> </td><td> 不按年龄 TTL 过期 </td></tr><tr><td> 从未被访问，且创建已超过 30 天 </td><td> 过期 </td></tr></tbody></table><p>过期的是：临时事实存了超过 30 天，而且一次都没被算进访问。不是“30 天没人看就过期”。没有按最近一次访问时间往前滑动的遗忘窗口。</p><h3 id="">第三阶段：相似整理</h3><p>这一阶段重新读取活动事实，按用户分开，用已有的有效向量两两算 cosine：</p><ul><li>≥ 0.75 的边用来形成连通簇；</li><li>超过 50 条的簇直接跳过；</li><li>先看能不能直接相似归档；</li><li>剩下的按默认 20 条一批交给模型。</li></ul><p>直接归档有两道限制。第一，簇里只要有临时事实或来源锚点，就不走纯向量直判。第二，选定要留下的那条之后，只有和它有 <strong>≥ 0.95 直接相似边</strong>的事实才归档——不能因为 A 连着 B、B 连着 C，就把 C 一起带走。</p><p>留哪一条按这个顺序比：先看是不是长期事实，再看重要程度，再看被用过多少次，最后用更新时间分胜负。长期事实有固定的优先，所以不是单纯留最新的，也不是单纯留被用得最多的。具体加权数字在现有记录里没有还原出来。</p><p>两条都在活动表里，字面不一样，精确去重不会动：</p><table><thead><tr><th> 编号 </th><th> 正文 </th></tr></thead><tbody><tr><td> m-1 </td><td> User mainly uses TypeScript and Node.js. </td></tr><tr><td> m-2 </td><td> User&#x27;s backend projects are primarily TypeScript and use pnpm. </td></tr></tbody></table><p>相似度落在 0.75 和 0.95 之间，我交给小模型。它返回一份计划：删掉 m-1 和 m-2，合成一条「User&#x27;s main stack is TypeScript and Node.js; backend projects use pnpm.」。改库的是我。新句子我写进活动表，旧的两行进归档，并指向这条新事实。模型没给替代句子却要求两行都删，这次我不执行。</p><p>模型只给我一份计划：哪些删，哪些合成。簇外面的编号我丢掉。它想把整簇删光又没有替代事实，我忽略。说了合成不等于删除，只有点名要删的我才归档。模型多给的到期时间我不收。合成事实我要先算好向量，这步失败就留着原事实。这一批里任意一条合成失败，原事实我都不归档。</p><p>合并我没做成一次事务。新事实写入、向量保存、旧事实归档是三次提交。中途失败会新旧并存。多条合成时，归档记录我只指向其中一个新事实。提示词要求尽量不丢信息，我没有证明过零损失。</p><h3 id="">第四阶段：归档清理</h3><p>归档超过 30 天的记录，我物理删除。我按归档时间算，不按原事实的创建时间。前面的存储阶段出错，我停住后面的阶段。整理失败了，我不能再把恢复来源删掉。</p><h2 id="">模型判断期间，数据已经变了</h2><p>想象这个顺序：</p><ol start="1"><li>Sleep 读到事实 A，<code>revision = 5</code>；</li><li>把 A 交给模型，判断它是否应该被合并；</li><li>模型思考的几十秒里，你顺手把 A 改了，版本号变成 6；</li><li>模型返回“归档 A”。</li></ol><p>我如果只按编号归档，会删掉你刚改过的新版本。把归档包进一个事务也不够。事务只保证这次提交要么全成要么回滚，不管模型看到的还是不是现在这份数据。</p><p>所以我把保护拆成三层：</p><p><img src="https://binzi.top/images/biny-memory/sleep-consistency.svg" alt="三层保护：SQLite 事务保证提交原子性，owner lease 保证执行权，expectedEntries 保证模型看到的事实没有过期"/></p><p><em>图 7：事务、租约、版本核对各管一件事。</em></p><p>同一个进程里，维护还在跑，我就不再开一轮。取消时我发一个中止信号。多个进程之间我用 SQLite 里的一把共享租约：</p><ul><li>租约 60 秒有效，我每 15 秒续一次；</li><li>每次写入我都带上持有者标记；</li><li>租约失效或被拿走之后，旧的那次不能再提交。</li></ul><p>真正提交时，我在事务里再对一遍当时看到的条目：还在活动、版本号没变、正文没变。有一项对不上我就拒绝。临时事实过不过期，我也在事务里再判一次。访问次数变了不会改版本号，我只对版本号，发现不了「扫描之后刚被召回」。</p><p>模型的判断可能已经过期。我提交时再拿当前数据对一次。这是乐观并发。</p><h2 id="">文件记忆、画像与人格</h2><p>长文本、用户画像、说话方式，我不放进事实表。这一层我不检索。每次拼提示时，我按文件在不在、内容空不空决定读不读。</p><p>文件我放在 <code>~/.config/biny/</code>：</p><table><thead><tr><th> 文件 </th><th> 我什么时候读 </th><th> 我放在哪 </th></tr></thead><tbody><tr><td> 用户画像 </td><td> 文件存在且不是空的。工作区里有同名文件，我用工作区那份 </td><td> 系统提示 </td></tr><tr><td> 长期备忘 </td><td> 不是空模板 </td><td> 贴在最新的用户消息前面 </td></tr><tr><td> 今天和昨天的日记 </td><td> 当天、昨天的日期文件存在且不是空的。更早的我不在这里读 </td><td> 和长期备忘放在一起 </td></tr><tr><td> 人格文件 </td><td> 有自定义内容 </td><td> 换掉我内置的人格 </td></tr><tr><td> 人物笔记 </td><td> 不在这条自动加载里 </td><td> 模型要用时自己去读 </td></tr></tbody></table><p>用户画像我放进系统提示。长期备忘和近两天日记我合成一块，跟在当前问题上。这里我不用向量挑段落，符合条件的文件整份放进去。更早的内容，要靠记忆搜索和会话搜索再查。</p><p>拼基础提示时，我按固定顺序读 <code>MEMORY.md</code>、今天的日记、昨天的日报，三者合计不超过 32,000 字符。顺序固定，长期备忘太长会挤掉近期日记。三个来源我没有各自留一份额度。</p><p><code>USER.md</code> 是独立的用户资料。我给它版本号、内容校验和历史文件。保存时要带上你看到的那个版本号，对不上我拒绝，避免两个界面互相覆盖。<code>SOUL.md</code> 是可选的自定义人格。没有这份文件，我用内置人格，不生成一份默认副本。</p><p>反思我只改已经存在的自定义人格，而且只动演化出来的特征：要有证据，单条最多 500 字，总共最多 15 条，每个自然日最多新增 1 条。修订或删除，我核对已有特征的版本号。改的是提示里的文字，不是模型参数。</p><p>日记我另算。普通聊天结束我不写每日笔记，要显式触发。回补时我用会话和回合的稳定键去重。反思可以读日记、允许的活动记录，以及最多 40 条辅助事实。已经是反思产物的，还有明确来自活动记录的事实，我排除，避免读到自己刚写的东西再写一遍。一次我最多收 3 条长期事实候选、2 项未完成承诺对应的行动。我先把草稿落盘，再逐项记下副作用有没有完成。抛异常的步骤，下次同源刷新时我重试。</p><p>定时提醒我默认关闭，打开后默认 30 分钟一次。它不调用模型写日记，只提醒该去做。</p><h2 id="crystal">Crystal：反复提到的东西</h2><p>我不用 Crystal 存事实，也不给它建关系边。事实里记「你在做某个项目」。我要做的是把这个项目长成一张可以引用的卡片。</p><p>一轮生成结束后，我看还有没有没处理过的用户消息。有的话，隔 400 毫秒做一次挖掘。我不会每轮都新建一张卡。</p><ol start="1"><li>我先拿已有卡片的名字去对这句。对上了，就把这句挂成材料。</li><li>没对上全名，我拿这句最多 800 字和卡片描述做点积。到了 0.62，我也挂上。所以「小王」和「王工」可以进同一张卡，不必先把全名再说一遍。</li><li>然后我让小模型只抽你自己用的称呼，2 到 24 个字。泛词我丢掉。</li><li>同一个称呼我按出现次数、跨了多少轮、跨了多少会话或多少天累计。三项都过线我才升级。你只在一个会话里反复提，我不当它已经稳定。</li><li>到了核心门槛，我生成一张候选卡，把相关的话都挂成材料。</li><li>小模型按类型补字段。我要求每条都能指回这次给它的材料，指不回的丢掉。我再检查必填和引用。</li><li>确认我单独做。这一轮我不会自动把它变成正式卡。正式之后，对话里才能当实体引用。14 天没有更新，我让它休眠。</li></ol><p>我按这个门槛升级：</p><table><thead><tr><th> 我把它从哪挪到哪 </th><th> 条件 </th></tr></thead><tbody><tr><td> 苗头到轮廓 </td><td> 提及 ≥ 4 次、跨 ≥ 3 个回合、分布 ≥ 2 </td></tr><tr><td> 轮廓到核心 </td><td> 提及 ≥ 8 次、跨 ≥ 5 个回合、分布 ≥ 2 </td></tr><tr><td> 自动休眠 </td><td> 14 天无更新 </td></tr></tbody></table><table><thead><tr><th> 阶段 </th><th> 条件 </th></tr></thead><tbody><tr><td> 苗头到轮廓 </td><td> 提及 ≥ 4 次、跨 ≥ 3 个回合、分布 ≥ 2 </td></tr><tr><td> 轮廓到核心 </td><td> 提及 ≥ 8 次、跨 ≥ 5 个回合、分布 ≥ 2 </td></tr><tr><td> 自动休眠 </td><td> 14 天无更新 </td></tr></tbody></table><p>分布我取会话数和日期数里较大的那个。跨两个会话，或者跨两天，我都算它铺开了，不要求两样同时达到。同一个词在同一次出现里我只计一次。</p><p>主题稳定之后，我按类型让它填清单：实体、概念、主张、流程、规则、项目。模型只能补空着的字段。我核对材料标记是不是这次给它的，回来后再读一遍卡片，避免盖掉你刚填的内容。清单完整、有来源、没有冲突，我才允许确认。自动补完的字段，我不会直接当成正式卡片。</p><h2 id="activity">Activity：记录你实际在电脑上做了什么</h2><p>我用 Activity 记你在电脑上做过什么。你的偏好和身份我放在事实和文件里，不从屏幕上猜。</p><h3 id="">我什么时候拍，什么时候不拍</h3><p>我不按固定间隔连拍。切应用、点击、打字停下来，这三件事才会让我准备拍一张。几秒内来了好几次，我只留最能说明「你换了一段活动」的那次：切应用高于点击，点击高于打字停顿。</p><table><thead><tr><th> 我看到什么 </th><th> 我做什么 </th></tr></thead><tbody><tr><td> 你切到另一个应用 </td><td> 清掉上一张画面的比较基线，把这次排进拍摄 </td></tr><tr><td> 你点了一下 </td><td> 记下坐标和按键，排队拍 </td></tr><tr><td> 你打字后停了约 1.2 秒 </td><td> 排队拍。按键我只留位置和修饰键，不留你打出的字 </td></tr><tr><td> 大约每 12 秒 </td><td> 看画面变没变，没变不存 </td></tr><tr><td> 大约每 2 分钟 </td><td> 存一张心跳。和上一张一样也存，免得你长时间不动之后断档 </td></tr><tr><td> 大约每 12 秒看一次浏览器 </td><td> 只读 Chrome、Safari 当前标签的标题 </td></tr></tbody></table><p>这些情况我直接不拍：</p><ul><li>你没开录制，或者屏幕锁着。</li><li>前台是密码管理器。我默认跳过钥匙串、1Password、Bitwarden、LastPass、Dashlane。焦点在上面时，我每 5 秒只记一条「这次跳过了」，不截图。</li><li>上一张还在拍，刚失败正在退避，或者还在几秒的合并窗口里。</li></ul><p>画面没变我分三步丢掉，便宜的判断放前面。整图颜色哈希一样，我就当没变。颜色分布差得很小，我当成光标闪烁或时钟跳动。最后才用 160×90 的缩略图逐像素比。你超过 30 秒没有输入，我把这种检查再放稀。</p><p>我存 jpg，宽不超过 2560。macOS 上我先用屏幕采集守护进程。它失败一次，这次运行我就改用 Electron 自带的截屏，不再反复试。</p><h3 id="">我把脱敏做在文字上</h3><p>我用系统自带的文字识别，语言包括英文、简体、繁体和日文。不是每张都做，默认每 3 张一次。识别出的文字我先脱敏，再入库，然后才在本地算向量。截图我留在本机，不发出去。</p><p>键盘我收得更早。全局监听只留按键位置，用来判断你是不是刚停手。你打出了什么字，我不记。</p><p>做会话分析和日报时，我发给模型的是脱敏后的识别文字，不是原图。原图我按时间降级：24 小时内留原图，一天到七天压到 1280×720，七天到三十天压到 640×360，超过三十天删掉。总量到 10GB，我清到 7.5GB。文字和向量留下，所以旧画面删了，我还能检索。</p><p>密码框我不做单独的画面打码。我靠两件事：前台是密码管理器时不拍；真拍到的画面最多留三十天。</p><p>碎片我归成一段一段的活动。小模型判断这段值不值得进长期事实，多数日常活动我就停在活动记录里。能进事实库的，我归一化后最多留 5 条候选，再走带语义判重的写入。查活动记录时，关键词和语义是两条路，不和事实召回混在一起。</p><p>日报我每 15 分钟看一次昨天写过没有，启动两分钟后先查一次。分析失败我就记失败，不编一份补上。我看到的生产记录里，这类失败大约占一半。</p><p>整理出来的记录像这样：</p><pre class=""><code class="">14:05  VS Code     打开 memoryStorage.ts
14:16  Terminal    运行 memory 测试
14:32  浏览器      查看 SQLite 文档
</code></pre>
<p>整理后的摘要可能是“用户下午主要在开发和验证 Agent 的 Storage 模块，期间查阅了 SQLite 相关资料”。</p><p>送给模型的是脱敏后的 OCR 和事件摘要。原始截图不发。截图和 OCR 向量留在本地。</p><p>分析可以写出很长的工作日志。能进长期事实库的很少：普通活动不值得半年后再召回。归一化后最多留 5 条候选，过「值不值得记」这道门，再走带语义判重的写入。</p><h2 id="">和别的实现比，我为什么这么选</h2><p>放在一起看，差在数据模型和谁来改：</p><table><thead><tr><th> 系统 </th><th> 核心存储与组织 </th><th> 写入、整理方式 </th><th> 读取方式 </th></tr></thead><tbody><tr><td> <strong>Biny</strong> </td><td> SQLite 事实 + 向量 + Markdown + 专用知识层 </td><td> 小模型提取、每日 Sleep </td><td> 每轮语义 Top-K，其他知识按需获取 </td></tr><tr><td> Codex </td><td> 状态数据库 + 分层 Markdown 记忆文件 </td><td> 两阶段后台提取与全局巩固 </td><td> 记忆摘要引导 + 渐进式读取 </td></tr><tr><td> Claude Code </td><td> 项目级 <code>MEMORY.md</code> + 主题 Markdown </td><td> 模型在会话中维护自动记忆 </td><td> 启动索引 + 按需读文件 </td></tr><tr><td> Mem0 </td><td> 事实条目 + 向量存储 + 元数据 </td><td> 事实提取及增删改管理 </td><td> 语义搜索、元数据过滤 </td></tr><tr><td> Letta </td><td> Memory Blocks / 文件式记忆 </td><td> Agent 主动维护 + sleep-time </td><td> 核心记忆注入、外部记忆检索 </td></tr><tr><td> OpenClaw </td><td> Markdown + SQLite 检索索引 </td><td> 对话写入 + Dreaming 整理 </td><td> 混合搜索、部分记忆自动注入 </td></tr><tr><td> Graphiti / Zep </td><td> 时序实体关系图 </td><td> 增量建图、事实失效与更新 </td><td> 混合检索 + 图遍历 </td></tr></tbody></table><p>Codex 有两阶段流水线（逐会话提取，再全局归并），Claude Code 会在会话里改项目记忆，OpenClaw 的 Dreaming 也默认凌晨三点跑。大家都有后台整理。差在记成什么、什么时候触发、读的时候怎么用。</p><p>Codex 和 OpenClaw 也用 SQLite，我也写了很多 Markdown。要看的是哪份数据说了算、一条记忆以什么为单位、谁能改、失效和并发怎么处理。Codex 留的是下一次写代码用得上的经验：仓库约定、失败过的方案、怎么验证。我这边留的是以后还要继续理解这个人。</p><p>图记忆我没用。记住稳定偏好，建关系、维护关系比多记住几条事实贵。等多跳关系和事实随时间失效变成主要问题时再上。现在是事实条目、时间锚点和 Crystal。没有 Graphiti 那种带有效区间和多跳查询的图。</p><p>mem0 可以自己托管，文件和 SQLite 也都能打开看，所以不是“为了本地才自己写”。我要把它嵌进 Biny 的消息树、时间锚点、Activity 来源开关、Crystal 清单、日记副作用恢复，还有“前台在忙，后台就别跑”。现成 SDK 给得了存储和召回，给不了这些接缝。</p><h2 id="">最后</h2><p>做下来觉得费劲的不是把信息存下来。是每一条都能查出从哪来、为什么还在、什么时候会被清掉。上面几层的数据来源不一样，一次向量搜索盖不住写入、召回和清理。</p><p>模型可以提出“这两条看起来是一回事”。删不删由程序定：候选范围、来源时间、版本核对、事务、租约。模型给判断，存储层落盘。</p><hr/><p><strong>项目地址：</strong> <a href="https://github.com/binyouo/Biny-Agent">Biny Agent</a></p><p><strong>实现索引：</strong></p><ul><li><a href="https://github.com/binyouo/Biny-Agent/blob/main/src/agent/context/memoryStorage.ts">记忆存储与归档提交</a> · <a href="https://github.com/binyouo/Biny-Agent/blob/main/src/agent/context/LocalMemory.ts">事实提取与 Sleep</a></li><li><a href="https://github.com/binyouo/Biny-Agent/blob/main/src/agent/context/MemoryEmbeddingService.ts">向量索引一致性</a> · <a href="https://github.com/binyouo/Biny-Agent/blob/main/src/agent/context/HybridMemoryRetriever.ts">事实召回</a></li><li><a href="https://github.com/binyouo/Biny-Agent/blob/main/src/agent/context/ContextMemory.ts">上下文压缩与预算</a> · <a href="https://github.com/binyouo/Biny-Agent/blob/main/src/agent/context/fileMemory.ts">文件记忆</a></li><li><a href="https://github.com/binyouo/Biny-Agent/blob/main/src/agent/context/crystalService.ts">主题结晶</a> · <a href="https://github.com/binyouo/Biny-Agent/blob/main/src/activity/analyzer.ts">活动分析</a></li><li><a href="https://github.com/binyouo/Biny-Agent/blob/main/src/agent/context/memoryTypes.ts">记忆数据契约</a> · <a href="https://github.com/binyouo/Biny-Agent/blob/main/src/personalization/index.ts">默认策略</a></li></ul></div></div>]]></description><link>https://binzi.top/posts/ai/biny-agent-memory-system</link><guid isPermaLink="true">https://binzi.top/posts/ai/biny-agent-memory-system</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 14 Oct 2026 11:30:00 GMT</pubDate></item><item><title><![CDATA[Ai Agent概念扫盲]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/ai-agent-concepts">https://binzi.top/posts/ai/ai-agent-concepts</a></blockquote><div><p>这篇按术语逐项解释常见的 AI Agent 概念。先看每项的简短说明，想了解完整流程、例子和边界，再进入对应专题。</p>
<h2 id="">协议与工具</h2><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">Skill</a>：</strong> 可复用的任务知识、流程与资源包。它告诉 Agent 一类事情应该怎么做，通常以 <code>SKILL.md</code> 为核心，还可以带脚本、模板和参考资料。比如代码审查 Skill 规定先看改动目标，再查影响、验证并输出报告。真正读文件和跑测试，仍需要工具。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">MCP</a>：</strong> Model Context Protocol，模型上下文协议，统一 AI 应用接入外部工具和资料的方式。比如把 GitHub 查询能力接到 Agent 应用里。MCP Server 可以提供工具、可读取的资源和提示模板；应用负责连接、发现和路由调用。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">ACP</a>：</strong> 这里指 Agent Client Protocol，Agent 客户端协议，连接编辑器等客户端与 Agent。客户端发送用户任务，Agent 返回进度、工具操作和权限请求。比如在一个编辑器里使用不同编码 Agent。看到 ACP 时先确认全称，因为也有其他同名缩写。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">A2A</a>：</strong> Agent2Agent，让调用方与远程 Agent 服务交换任务、状态和交付物。比如把一项资料调查交给远程 Agent，再取得报告。MCP 侧重接入工具和资料，ACP 侧重客户端交互，A2A 侧重远程 Agent 的任务互操作。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">LSP</a>：</strong> Language Server Protocol，语言服务器协议，提供代码定义、引用、类型、补全和错误诊断等语言服务。文本搜索能找到同名字符串，LSP 可以帮助确定某处引用究竟指向哪个定义。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">DAP</a>：</strong> Debug Adapter Protocol，调试适配器协议，提供断点、单步、变量和调用栈等调试能力。LSP 帮助理解代码，DAP 帮助观察程序运行过程。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">Tool</a>：</strong> 工具，是 Agent 可以使用的一项具体操作能力，比如读文件、查数据库、运行命令或操作浏览器。模型通过工具取得外部信息，或让外面的程序执行真实动作。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">Tool Call</a>：</strong> 工具调用，是使用某个工具的一次具体请求。<code>read_file</code> 是工具，读取 <code>/repo/login.ts</code> 是一次 Tool Call。调用请求、工具执行结果和用户任务完成，是不同状态。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">Function Calling</a>：</strong> 函数调用机制，让模型用结构化格式表达「调用哪个函数、传什么参数」。通常由应用执行函数，再把结果交回模型；托管工具则由提供方执行相应操作。它负责表达调用，MCP 负责一种外部能力接入方式。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">JSON Schema</a>：</strong> 描述数据需要具备哪些字段、类型和约束。例如工具参数里的 <code>path</code> 必须是字符串。程序按 Schema 校验，可以发现格式问题，但还要另行检查路径是否存在、是否允许访问。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">Structured Output</a>：</strong> 结构化输出，让模型按照约定结构生成数据，例如把审查结果输出成一个问题数组。它方便程序处理，格式符合要求之后，事实和业务条件仍需验证。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">Tool Search</a>：</strong> 工具搜索，从较多工具中寻找当前任务需要的能力，再加载相关定义。比如当前只需要查工单，就先找到工单查询工具，减少无关工具说明进入上下文。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">Progressive Disclosure</a>：</strong> 渐进式披露，先给名称和简介，需要时再加载完整说明和附件。读 Skill 时可以先看目录，确定相关后再读正文。Lazy Loading、Deferred Loading 也涉及按需或延后加载，但要确认延迟的是文档、工具定义、连接还是进程。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">Code Mode</a>：</strong> 让模型先写程序，再由执行环境运行程序，用循环、条件、筛选和聚合组织工具调用。比如一段代码查询多个项目，只返回高优先级工单。它可能减少模型往返和中间数据，但也需要处理代码错误、权限和执行状态。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">PTC</a>：</strong> Programmatic Tool Calling，程序化工具调用，用程序组织工具操作的相关思路，也可能特指某家提供方的实现。理解时要继续看程序在哪里运行、怎样调用宿主工具、哪些结果回到模型。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">Plugin / Extension</a>：</strong> 插件或扩展，用来安装和分发附加能力。一个插件可以同时包含 Skill、工具连接配置、命令或 Hook。Hook 在特定事件触发逻辑，Command 则提供显式命令入口。</p><h2 id="agent-">Agent 与工作流</h2><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-loop-runtime/">Agent</a>：</strong> 在本系列的 LLM 应用语境中，指围绕目标，根据任务与反馈选择行动、使用工具并推进工作的系统。比如发现测试失败后，自行查日志、定位原因、修改并重新验证。模型负责判断，外围程序负责执行和状态管理。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-loop-runtime/">Workflow</a>：</strong> 工作流，预先组织步骤与分支。比如「解析文档 → 检索 → 生成 → 校验」。Agent 更侧重根据反馈动态决定下一步；实际系统经常把工作流与 Agent 组合使用。</p><h2 id="">模型与推理</h2><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-model-basics/">LLM</a>：</strong> Large Language Model，大语言模型，用于理解输入和生成内容。它可以生成文字、代码和工具参数。聊天产品通常还包含工具、历史存储和执行程序，产品与模型不是同一个对象。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-model-basics/">Token</a>：</strong> 模型处理文本时使用的单位，不固定等于一个汉字、单词或字符。Tokenizer 负责文本与 Token 编号之间的转换，Input Tokens 和 Output Tokens 分别描述输入、输出的计量。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-model-basics/">Context Window</a>：</strong> 上下文窗口，一次模型调用可容纳的上下文范围。容量大不代表其中的信息都能被有效利用；输入、输出和推理预算怎样计算，应看对应接口。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-model-basics/">Inference / Reasoning</a>：</strong> Inference 指运行已训练的模型来计算输出；Reasoning 更强调分析和逻辑推导。两者都可能译成「推理」，但普通回答也会执行 Inference，不一定涉及复杂 Reasoning。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-model-basics/">Prefill / Decode</a>：</strong> Prefill 处理已有输入，Decode 逐步生成后续 Token。长输入和长输出带来的耗时可能发生在不同阶段；Streaming 则把内容或事件逐段返回，提供更早的反馈。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-model-basics/">Temperature / Top p / Top k</a>：</strong> 控制生成采样的参数，影响候选 Token 的分布和选择范围。低 Temperature 不保证答案正确；生成采样的 Top k 与检索返回数量 Top K，属于不同环节。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-model-basics/">Fine-tuning</a>：</strong> 微调，通过训练调整模型参数来适应任务或数据。SFT、偏好优化等涉及训练方法，LoRA 是参数高效适配的一种方法。读 Skill、查 RAG、保存记忆通常属于运行时信息组织，没有直接更新模型权重。</p><h2 id="">上下文与记忆</h2><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-context-memory/">Prompt</a>：</strong> 当前任务的指令与输入，有些语境也泛指整份模型输入。例如「只根据这份文档回答，资料没有提到的内容标为未知」。Prompt Engineering 关注怎样把目标、例子、约束和输出要求写清楚。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-context-memory/">Context</a>：</strong> 上下文，本次模型调用实际可用的信息，可能包括用户问题、历史、工具定义、工具结果、检索资料和记忆。信息保存在系统里，不代表这次一定交给了模型。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-context-memory/">History</a>：</strong> 历史交互记录，例如过去的用户消息、模型回答和工具结果。界面显示的历史可以比本次模型输入更多，系统通常需要选择、裁剪或压缩后再发送。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-context-memory/">Memory</a>：</strong> 记忆，为后续任务保留和使用的信息，例如用户偏好、项目约束和过去的经验。它可以存在文件或数据库里，不一定要用向量库。保存之后，还需要按作用域检索、处理冲突和检查时效。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-context-memory/">Context Engineering</a>：</strong> 上下文工程，管理什么信息、在什么时候、以什么形式进入模型。例如把完整日志放到文件里，先提供失败摘要和读取入口，让模型需要时再查细节。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-context-memory/">Compaction</a>：</strong> 上下文压缩，把较长历史整理成继续工作所需的材料。应保留目标、约束、进度、决定和验证结果。它可能丢失细节，摘要也不能替代已经变化的文件和真实业务状态。</p><h2 id="-rag">检索与 RAG</h2><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-rag/">RAG</a>：</strong> Retrieval-Augmented Generation，检索增强生成，先获取外部依据，再结合依据回答。比如先检索公司的最新报销制度，再解释额度。完整链路包括资料准备、检索、排序、证据组织、生成与核查。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-rag/">Chunking</a>：</strong> 分块，把资料切成适合检索和放入上下文的片段。块太小可能丢掉条件，块太大可能混入无关内容。例如「500 元」应尽量保留城市、日期和适用范围。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-rag/">Embedding</a>：</strong> 嵌入，把文本、图片等内容转成数值表示，供检索等任务比较。语义向量可以帮助找到字面不同但含义接近的内容，不过向量相似不证明它能回答问题。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-rag/">BM25</a>：</strong> 常见的关键词相关性评分方法，利用词项匹配、词频和文档长度等信号。错误码、文件编号、函数名等精确术语，往往需要这类匹配能力。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-rag/">Vector Database / ANN</a>：</strong> 向量数据库提供向量存储、索引和查询等能力；ANN 是近似最近邻搜索，以一定近似换取速度和资源收益。HNSW、IVF、PQ 是相关索引与压缩方法，主要解决检索效率问题。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-rag/">Hybrid Search</a>：</strong> 混合检索，结合关键词、向量等多种检索方法。比如一边匹配制度编号，一边理解「住酒店能报多少」的含义，再合并候选。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-rag/">RRF</a>：</strong> Reciprocal Rank Fusion，倒数排名融合，利用文档在不同结果列表里的名次合并排序，避免直接比较不同尺度的原始分数。它汇总排名信号，不直接判断内容是否正确。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-rag/">Reranker</a>：</strong> 重排器，对已经召回的候选做更细的相关性判断并重新排序。Cross-Encoder 是一种常见模型形式，会联合处理问题与候选内容。正确资料没有进入候选集时，重排无法把它找回来。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-rag/">Grounding / Citation</a>：</strong> Grounding 要求具体说法由证据支持，Citation 标明来源。链接存在之后，还要确认来源支持对应说法，包含当前日期和适用条件。证据不足时可以明确无法确认，这叫 Abstention。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-rag/">GraphRAG</a>：</strong> 利用图结构辅助检索与生成的一类方法。知识图谱组织实体及其关系；微软 GraphRAG 是一个具体实现，还涉及社区报告等索引。使用图数据库，不自动等于实现了该系统。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-rag/">Agentic RAG</a>：</strong> 让 Agent 参与决定是否检索、查哪里、怎样改写问题、是否继续，以及证据是否足够。查到旧制度后继续寻找新版本，是一个简单例子。Self-RAG、Corrective RAG 则有各自的研究机制，不能仅凭名称视为同一种方法。</p><h2 id="">运行与状态</h2><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-loop-runtime/">Agent Loop</a>：</strong> Agent 的控制循环，准备上下文、调用模型、执行工具、取得观察，再决定继续、等待或结束。一次模型生成结束，不代表整个任务已经完成。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-loop-runtime/">ReAct</a>：</strong> Reasoning 与 Acting，强调判断和行动交替推进。比如原先怀疑网络故障，读日志后发现数据库权限错误，随后调整排查方向。它与前端 UI 库 React 是不同概念。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-loop-runtime/">Engineering Loop</a>：</strong> 工程闭环，从目标和验收标准出发，实施变更、获取真实反馈、根据反馈修正并交付。测试、业务状态和界面检查可以提供反馈。不断修改却从不验证，闭环就缺少重要一环。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-loop-runtime/">Runtime</a>：</strong> 运行环境。语言 Runtime 可以指 Node.js、Python 等环境；Agent Runtime 则承载 Agent 循环、工具执行、状态和生命周期。具体可以是库、服务或多个组件。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-loop-runtime/">Harness</a>：</strong> 围绕模型组织执行与反馈的外围机制，常涉及指令、工具接口、上下文、调度、权限、恢复和验收。Harness Engineering 关注怎样让这些部分配合，使任务能够持续、正确、可验证地推进。它与 Runtime 的范围有重叠。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-loop-runtime/">Session / Turn / Run</a>：</strong> Session 是带标识的交互或执行上下文，Turn 是一轮交互，Run 是一次运行实例，具体边界依系统定义。Process 是操作系统进程，一个进程可以服务多个 Session，一个 Run 也可以启动多个工具进程。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-loop-runtime/">State</a>：</strong> 状态，记录任务或系统现在处于什么阶段，例如运行中、等待输入、失败或完成。判断某类事实时，以对应的 Source of Truth，也就是权威记录为准；工单是否存在，应核对工单系统。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-loop-runtime/">Persistence / Checkpoint</a>：</strong> Persistence 是持久化，让进程退出后数据仍可读取；Checkpoint 是保存从某个边界恢复所需的状态。保存聊天历史不等于保存了完整工具进度，也不保证能从任意一行代码继续。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-loop-runtime/">Resume / Reconnect</a>：</strong> Resume 从可恢复边界继续工作，Reconnect 恢复通信连接。界面重新连接成功，不代表后台任务重新执行，也不代表恢复机制完整。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-loop-runtime/">Retry / Idempotency</a>：</strong> Retry 是重试，Idempotency 是幂等性，在约定范围内重复同一请求，其预期业务效果与执行一次相同。超时不证明操作未成功；响应丢失后可以先核对真实状态，再决定是否重试。</p><h2 id="">协作与界面</h2><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-collaboration-computer-use/">Multi-agent / Subagent</a>：</strong> Multi-agent 是多个 Agent 协作，Subagent 是被委派子任务的 Agent。它们可以分工、独立审查或并行处理，但需要明确上下文、文件责任和结果汇总方式；数量增加也会增加协调成本。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-collaboration-computer-use/">Handoff</a>：</strong> 把控制权交给另一个 Agent，例如客服把后续处理移交给退款 Agent。让子 Agent 返回一份报告则通常属于 Agent-as-a-Tool，主 Agent 收到结果后继续负责，交互语义不同。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-collaboration-computer-use/">Computer Use / ACI</a>：</strong> Computer Use 是通过工具观察和操作电脑界面的能力，可利用 DOM、可访问性树、截图和鼠标键盘。ACI 是 Agent 与计算机的交互接口设计，关注观察和操作怎样帮助模型完成任务。点击保存之后，还要确认真实保存结果。</p><h2 id="">缓存与观测</h2><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-cache-observability/">Cache Hit</a>：</strong> 缓存命中，找到满足复用条件的计算状态或结果。它可能命中输入前缀计算、完整答案、工具结果或工具目录，必须先说明缓存对象。命中率高不自动证明整个任务更快。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-cache-observability/">KV Cache</a>：</strong> 保存注意力计算中的 Key、Value 表示，让自回归模型生成后续 Token 时复用已有计算。这里的 KV 是模型中间表示，不是保存用户偏好的长期记忆数据库。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-cache-observability/">Prompt Cache / Response Cache</a>：</strong> Prompt Cache 通常复用符合条件的稳定输入前缀计算，模型仍生成新输出；Response Cache 复用完整回答。前缀命中缓存后，里面的信息仍计入逻辑上下文，不会扩大上下文窗口。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-cache-observability/">Trace / Logs / Metrics</a>：</strong> Trace 关联一次过程中的多个操作，Span 描述其中一段操作；Logs 记录离散事件，Metrics 汇总数量和分布。它们帮助判断哪里慢、哪里失败，持久恢复仍需要另外的状态与执行机制。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-cache-observability/">Latency / TTFT</a>：</strong> Latency 是从指定起点到终点的耗时，TTFT 是首 Token 延迟，端到端延迟则看任务提交到完成的总时间。首字出现得快，后面的工具和外部任务仍可能让总时间很长。</p><h2 id="">安全与评测</h2><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-safety-evaluation/">Sandbox / Approval</a>：</strong> Sandbox 限制代码能接触的资源；Approval 确认这次具体动作是否获准。Authentication 确认身份，Authorization 确认可执行范围。这几层需要配合，连接工具或读到 Skill 不会自动授予额外权限。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-safety-evaluation/">Prompt Injection</a>：</strong> 提示注入，用输入或外部资料中的指令诱导 Agent 偏离任务或越权。例如网页要求读取密钥，这仍是网页内容，不能自行变成用户授权。Trust Boundary，信任边界，规定不同来源的信息具有怎样的权威。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-safety-evaluation/">Test / Eval / Verifier</a>：</strong> Test 检查系统行为，Eval 评估一组任务上的表现，Verifier 检查当前结果是否达到验收要求。Agent 说「创建成功」之后，应核对目标对象是否正确存在；测试通过也要说明覆盖了哪些行为。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-safety-evaluation/">Acceptance Criteria</a>：</strong> 验收标准，明确什么结果才算完成。比如登录修复要求正确凭据通过、错误凭据被拒绝、旧登录方式仍兼容。Spec 描述预期行为与约束，PRD 描述用户、问题、范围和目标。</p><p><strong><a href="https://binzi.top/2026/10/12/ai-agent-safety-evaluation/">Baseline / Ablation</a>：</strong> Baseline 是比较基线，Ablation 是消融，通过去掉或替换一个机制检查它的贡献。比如用同一查询集比较有无重排，才能判断新增 Reranker 是否带来收益。</p><p>想继续看已有文章，可以读 <a href="https://binzi.top/2026/04/14/MCP%E5%92%8CSkill%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88%E5%85%B3%E7%B3%BB/">MCP 和 Skill，到底是什么关系？</a>、<a href="https://binzi.top/2026/07/09/RRF%E6%8E%92%E5%BA%8F%E8%9E%8D%E5%90%88/">RRF 排序融合</a>和 <a href="https://binzi.top/2026/04/10/%E6%80%8E%E4%B9%88%E5%87%8F%E5%B0%91%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%B9%BB%E8%A7%89/">怎么减少大模型幻觉？</a>。各专题附官方规范、项目文档和原始论文。</p></div></div>]]></description><link>https://binzi.top/posts/ai/ai-agent-concepts</link><guid isPermaLink="true">https://binzi.top/posts/ai/ai-agent-concepts</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 12 Oct 2026 13:40:00 GMT</pubDate></item><item><title><![CDATA[Ai Agent概念扫盲：Token、推理、采样与模型训练]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/ai-agent-model-basics">https://binzi.top/posts/ai/ai-agent-model-basics</a></blockquote><div><p><a href="https://binzi.top/2026/10/12/ai-agent-concepts/">返回总览</a> · 本篇讨论模型本身，工具接入见 <a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">下一篇</a>。</p><p>让 Agent「读报错、改代码、跑测试」，其中理解报错和生成修改建议的，通常是语言模型；真正读取文件和运行测试的，是外围程序。</p><p>这一篇先把模型这一层的词讲清楚。我们用「给模型一段报错，让它分析原因」这个简单场景，贯穿输入、生成和训练。</p>
<h2 id="llmmodelweights">LLM、Model、Weights，先分清模型和产品</h2><p><strong>LLM，全称 Large Language Model，是大语言模型。</strong> 常见自回归语言模型根据已有内容，逐步生成接下来的 Token。生成结果可以是解释、代码或工具参数。</p><p><strong>Model</strong> 是训练得到的计算模型；<strong>Parameters / Weights</strong> 是模型内部的参数和权重。它们不是你在请求里填写的 <code>temperature</code>、输出上限等配置参数。</p><p>聊天产品还包括界面、模型请求、工具、账户、历史存储等部分。换一个聊天界面不等于换模型；同一个模型搭配不同工具和上下文，完成任务的表现也可能不同。</p><p>这个区别很实用。Agent 查不到文件时，原因可能是根本没有文件工具；补上工具不需要重新训练模型。</p><h2 id="token">Token，模型怎样计算输入和输出</h2><p><strong>Token 是模型处理文本时使用的单位。Tokenizer 是文本与 Token 编号之间的转换器。</strong> 一段中文、英文、代码会怎样切分，要看具体 Tokenizer，不能统一按字符数换算。</p><table><thead><tr><th> 名称 </th><th> 在报错分析任务里是什么 </th></tr></thead><tbody><tr><td> Input Tokens </td><td> 用户问题、报错、指令、历史以及可能附带的工具定义 </td></tr><tr><td> Output Tokens </td><td> 模型生成的解释、代码或工具参数 </td></tr><tr><td> Reasoning Tokens </td><td> 某些模型对内部推理资源的计量，展示和预算口径依提供方定义 </td></tr><tr><td> Context Window </td><td> 本次调用可以容纳的上下文范围，输入输出如何共用额度依接口定义 </td></tr></tbody></table><p>报错只有十行，不代表输入只有十行。应用可能还带上几百条历史消息和大量工具定义。</p><p>上下文窗口足够大，也不能保证所有信息都被有效利用。相关内容被无关日志包围时，仍可能影响判断。具体怎样整理信息，见 <a href="https://binzi.top/2026/10/12/ai-agent-context-memory/">上下文专题</a>。</p><p>文本生成及输入编码可参考 <a href="https://huggingface.co/docs/transformers/llm_tutorial">Hugging Face 的 Text generation 文档</a>。</p><h2 id="inference--reasoning">Inference 和 Reasoning，为什么都译成推理</h2><p><strong>Inference 指使用已经训练好的模型计算输出。Reasoning 更强调多步分析和逻辑推导。</strong></p><p>模型回答「你好」也做了一次 Inference，但通常不需要复杂 Reasoning。排查一个跨服务故障，则可能需要比较多个假设。</p><p>一次自回归生成常分为 <strong>Prefill，预填充</strong> 和 <strong>Decode，解码生成</strong>。前者处理已有输入，后者逐步生成后续 Token。</p><p>给模型一大份日志，可能增加输入处理开销；让模型写一份很长的报告，可能增加生成开销。两者都让人感觉「慢」，却不发生在同一阶段。</p><p><strong>Streaming，流式输出</strong>，把内容或事件逐段返回。它改善等待时的反馈，但不自动减少整个任务的总耗时。</p><h2 id="temperaturetop-ptop-k">Temperature、Top p、Top k，控制的是采样</h2><p>模型生成下一步内容时，会对候选 Token 给出概率等信号，再按照生成策略选择。</p><p><strong>Temperature</strong> 调整分布的集中或分散程度。较低设置通常让采样更集中；较高设置可能增加变化。它不能把错误知识改成正确知识。</p><p><strong>Top p</strong> 按累计概率保留一组候选；<strong>Top k</strong> 保留概率最高的 K 个候选。这两个 K 和检索中「取前 K 篇文档」的 K，处在不同环节。</p><p><strong>Seed</strong> 控制部分随机过程。相同 Seed 并不保证模型版本、计算环境和所有服务行为变化后还能完全复现。</p><p>下面三个限制也要分开。</p><table><thead><tr><th> 配置或信号 </th><th> 限制或说明什么 </th></tr></thead><tbody><tr><td> Reasoning Effort </td><td> 某些模型的思考资源控制，具体档位语义依接口定义 </td></tr><tr><td> Max Output Tokens </td><td> 单次模型生成的输出上限 </td></tr><tr><td> Agent 的 maxSteps </td><td> 外围程序的执行步数上限，步数怎么计要看实现 </td></tr><tr><td> Stop Reason </td><td> 这一次生成为什么停止，比如输出完毕、达到上限或请求工具 </td></tr></tbody></table><p>一次生成结束之后，Agent 可能还需要运行工具、再发起下一次生成。所以看到 Stop Reason，不能直接认定用户任务已完成。</p><h2 id="-vlm">多模态和 VLM，能看和能生成要分别确认</h2><p><strong>Multimodal，多模态</strong>，表示系统支持不止一种信息模态，例如文字、图片、音频。</p><p><strong>VLM，Vision-Language Model，视觉语言模型</strong>，可以结合图像与文字理解内容。例如根据截图指出保存按钮的位置。</p><p>支持图片输入，不自动表示支持图片输出；能读截图，也不自动表示能够操作电脑。点击需要工具，点击之后的业务结果还要检查。</p><h2 id="">预训练、后训练和微调，改变模型参数</h2><p><strong>Pretraining，预训练</strong>，通常从大量数据中学习基础能力。<strong>Post-training，后训练</strong>，继续调整指令遵循、偏好或推理等能力。它是较宽的阶段称呼，不等于某一种算法。</p><p><strong>Fine-tuning，微调</strong>，通过训练调整参数，让模型适应任务或数据。它可以发生在后训练体系中。</p><table><thead><tr><th> 名称 </th><th> 简单理解 </th><th> 示例 </th></tr></thead><tbody><tr><td> SFT，监督微调 </td><td> 用输入和期望输出等样本训练 </td><td> 用一批规范的客服问答学习回答方式 </td></tr><tr><td> RL，强化学习 </td><td> 根据奖励等反馈优化策略 </td><td> 对可验证任务的结果给反馈 </td></tr><tr><td> RLHF </td><td> 利用人类反馈的强化学习相关方法 </td><td> 用人的偏好参与训练反馈体系 </td></tr><tr><td> RLAIF </td><td> 利用 AI 反馈的相关方法 </td><td> 用模型评价提供部分反馈 </td></tr><tr><td> DPO，直接偏好优化 </td><td> 利用成对偏好数据优化模型的一类方法 </td><td> 给出同一问题下更好和更差的回答 </td></tr><tr><td> PEFT，参数高效微调 </td><td> 只训练较少参数等方法的统称 </td><td> 降低完整微调的资源开销 </td></tr><tr><td> LoRA </td><td> 用额外的低秩参数学习适配变化 </td><td> 冻结基础权重，训练较小的适配参数 </td></tr></tbody></table><p>这些名字并不是一条必须依次走完的流水线。DPO 与某种 RLHF 流程可以是不同方案；LoRA 描述参数怎样适配，可以与不同训练目标组合。<a href="https://huggingface.co/docs/trl/index">TRL 文档</a>介绍了多个训练方法，<a href="https://huggingface.co/docs/peft/main/en/conceptual_guides/lora">PEFT 的 LoRA 文档</a>解释了低秩适配。</p><h2 id="-skill-rag">给提示词、读 Skill、查 RAG，通常没有重新训练</h2><p>如果你把「请只根据日志回答」写进输入，模型的行为可能发生变化，但通常没有更新权重。</p><p><strong>In-context Learning，上下文学习</strong>，指模型通过当前输入的说明和示例适应任务。<strong>Zero-shot</strong> 不提供任务示例；<strong>Few-shot</strong> 给少量示例，比如先展示两组规范的报错分析。</p><p>读取 Skill、检索文档、把旧经验放进上下文，也通常属于运行时信息组织。它们能让模型拿到更好的依据，却不是微调。</p><p>要记住最新接口文档，RAG 常是资料检索这一层的方案；要让某种任务行为稳定下来，则可能涉及提示词、流程、评测，甚至训练。选择方案前，先确认缺的是知识、方法、工具还是模型能力。</p><h2 id="moe">蒸馏、量化、MoE，各自改变什么</h2><p><strong>Distillation，蒸馏</strong>，让学生模型学习教师模型的行为。目标可能是保留部分能力，同时减少资源需求；效果要对任务实测。</p><p><strong>Quantization，量化</strong>，降低部分数值表示的精度，减少存储或计算资源需求。不同精度和实现对效果、速度的影响不同，不能直接说「位数减半，速度就翻倍」。</p><p><strong>MoE，Mixture of Experts，混合专家模型</strong>，在模型内部使用多个专家模块与路由机制。这里的专家是神经网络组件。</p><p>多 Agent 里的「代码审查专家」则是一个任务角色，可能有自己的上下文和工具。MoE 和多 Agent 属于不同层次，不能从「模型有多个专家」推断应用里运行了多个 Agent。</p><h2 id="">回到那个报错任务</h2><p>报错进入 Context，Tokenizer 把输入编码为 Token；模型执行 Inference，生成分析或工具请求；工具获取新证据后，应用重新组织下一次输入。</p><p>这条链路里，读取文档通常不会改变模型权重，降低 Temperature 不能代替证据，增加输出预算不能代替工具。把三件事分清楚，就知道下一步应该调整哪一层了。</p><p><a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">下一篇：工具、Skill 和协议怎样接起来</a> · <a href="https://binzi.top/2026/10/12/ai-agent-concepts/">返回总览</a></p></div></div>]]></description><link>https://binzi.top/posts/ai/ai-agent-model-basics</link><guid isPermaLink="true">https://binzi.top/posts/ai/ai-agent-model-basics</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 12 Oct 2026 13:39:00 GMT</pubDate></item><item><title><![CDATA[Ai Agent概念扫盲：Tool、Skill、MCP、ACP 与 Code Mode]]></title><description><![CDATA[<link rel="preload" as="image" href="https://binzi.top/images/ai-agent-concepts/tool-call.svg"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/ai-agent-tools-protocols">https://binzi.top/posts/ai/ai-agent-tools-protocols</a></blockquote><div><p><a href="https://binzi.top/2026/10/12/ai-agent-concepts/">返回总览</a> · 上一篇 <a href="https://binzi.top/2026/10/12/ai-agent-model-basics/">模型基础</a>。</p><p>假设你要 Agent 审查一个仓库。</p><p>它需要知道怎样审查，也需要真的能读代码。代码放在远程平台时，还需要一条接入路径。任务复杂以后，又需要把多次操作组织起来。</p><p>Tool、Skill、MCP、Code Mode 就在解决这些不同问题。理解它们最方便的方式，是沿一次实际调用往下看。</p>
<h2 id="tooltool-callfunction-calling">Tool、Tool Call、Function Calling</h2><p><strong>Tool 是暴露给 Agent 的可执行能力。</strong> 读文件、查工单、运行测试，都可以是工具。</p><p><strong>Tool Call 是一次具体调用请求。Function Calling 是让模型按结构化格式选择函数并生成参数的机制。</strong> 有的 API 把 Function Calling 和 Tool Calling 近似使用；具体接口还可能有托管搜索等工具类型。</p><p>下面只是抽象格式，不是某家 API 的完整请求。</p><pre class="language-json lang-json"><code class="language-json lang-json">{
  &quot;id&quot;: &quot;call_01&quot;,
  &quot;name&quot;: &quot;read_file&quot;,
  &quot;arguments&quot;: { &quot;path&quot;: &quot;/repo/src/login.ts&quot; }
}
</code></pre>
<p>模型表达了「读这个文件」的意图。执行器还要检查参数、权限和文件是否存在，调用真实代码，再把结果返回。自定义工具与提供方托管工具的执行位置不同，可以参考 <a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview">Claude 的工具机制文档</a>。</p><p><img src="https://binzi.top/images/ai-agent-concepts/tool-call.svg" alt="模型表达调用，宿主检查和执行，结果回到模型" height="0" width="0"/></p><p>比如读文件成功，只说明拿到了内容；运行测试成功，还要看退出状态和测试结论；用户要求的 Bug 是否解决，又是下一层判断。</p><h2 id="">工具调用周围的一圈词</h2><table><thead><tr><th> 名称 </th><th> 负责什么 </th><th> 在读文件时是什么 </th></tr></thead><tbody><tr><td> Tool Definition </td><td> 工具的名称、用途和参数结构 </td><td> <code>read_file</code> 的接口说明 </td></tr><tr><td> Description </td><td> 告诉模型何时用、怎样用 </td><td> 文件过大时怎样分页读取 </td></tr><tr><td> Arguments </td><td> 本次具体参数 </td><td> 路径、起始行和读取长度 </td></tr><tr><td> Tool Call ID </td><td> 关联调用与结果 </td><td> <code>call_01</code> 对应这一份结果 </td></tr><tr><td> Tool Registry </td><td> 管理可用工具及实现 </td><td> 名称到处理函数的注册表 </td></tr><tr><td> Handler / Executor </td><td> 真正执行操作 </td><td> 打开文件并读取内容的代码 </td></tr><tr><td> Tool Result </td><td> 返回内容、状态或错误 </td><td> 文本、截断说明、错误原因 </td></tr><tr><td> Tool Choice </td><td> 引导或限制模型选工具 </td><td> 允许自动选择或指定某工具 </td></tr><tr><td> Tool Router </td><td> 选择工具或执行路径 </td><td> 把代码查询路由到语言服务 </td></tr><tr><td> Middleware </td><td> 统一处理横切逻辑 </td><td> 校验、权限、日志、超时 </td></tr></tbody></table><p>工具在 Registry 中存在，不代表模型已经看到了它的完整定义；看到了定义，也不代表当前用户有权执行。目录、模型输入和权限是三层。</p><h2 id="jsonschema--structured-output">JSON、Schema 和 Structured Output</h2><p><strong>JSON 是数据表示格式。JSON Schema 描述数据应当有什么字段、类型和约束。Validation 是按照规则校验。</strong></p><p>例如 <code>path</code> 必须是字符串，<code>limit</code> 必须是正整数。<strong>Structured Output，结构化输出</strong>，让模型按约定结构生成结果，方便下一段程序处理。</p><p>审查报告输出为「问题数组」会比一大段自由文本更容易展示，但至少还要检查三层。</p><table><thead><tr><th> 检查层 </th><th> 例子 </th></tr></thead><tbody><tr><td> 语法 </td><td> 是不是合法 JSON </td></tr><tr><td> 结构 </td><td> <code>severity</code> 是否属于允许的枚举 </td></tr><tr><td> 业务与权限 </td><td> 引用行号是否存在、问题是否真实、路径能否访问 </td></tr></tbody></table><p>符合 Schema 只解决结构层问题。「问题在第 20 行」可以格式完全正确，但第 20 行可能根本没有问题。</p><p><strong>Serialization / Deserialization，序列化与反序列化</strong>，是对象与可传输表示之间的转换。<strong>JSON-RPC</strong> 则规定方法、请求 ID、结果和错误怎样交换；<strong>OpenAPI</strong> 描述 HTTP API 的路径和请求响应结构。它们不能仅因为都能使用 JSON 就看成同一种东西。</p><h2 id="skill">Skill，方法和资源怎样复用</h2><p><strong>Skill 是可复用的任务知识、流程与资源包。</strong> 在 <a href="https://agentskills.io/specification">Agent Skills 格式</a>中，核心是 <code>SKILL.md</code>，还可以附脚本、参考文档和模板。</p><p>代码审查 Skill 可以规定，先明确改动目标，再沿调用链找影响，运行必要检查，最后提供问题与证据。</p><p>它提供的是工作方法。读取代码要靠文件或仓库工具，运行脚本要靠执行环境。即使 Skill 写着「使用数据库工具」，也不能自动产生数据库访问权限。</p><p><strong>Prompt</strong> 是当前任务的指令和输入；Skill 把反复使用的方法放进可维护的包。<strong>Command</strong> 提供显式触发入口；<strong>Hook</strong> 在特定生命周期事件触发逻辑；<strong>Plugin / Extension</strong> 是安装和分发附加能力的包；<strong>Connector / Adapter</strong> 负责外部服务连接或接口适配。</p><p>一个插件可以同时带 Skill、MCP 配置和命令。它们是可以组合的部件。</p><p>已有的 <a href="https://binzi.top/2026/04/14/MCP%E5%92%8CSkill%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88%E5%85%B3%E7%B3%BB/">MCP 和 Skill，到底是什么关系？</a>包含更长的审查案例。</p><h2 id="">为什么工具很多时要先看目录</h2><p><strong>Progressive Disclosure，渐进式披露</strong>，先让模型看到名称和适用场景，相关时再读取完整说明，需要时再打开附件。这和查手册先看目录一样。</p><p><strong>Tool Search</strong> 从较多工具里寻找当前需要的能力；<strong>Tool Preselection</strong> 在主执行前先筛选候选；<strong>Deferred Loading</strong> 延后加载工具定义等内容；<strong>Dynamic Discovery</strong> 在运行中发现或更新能力；<strong>Lazy Loading</strong> 则是更广泛的按需加载思路。</p><p>例如应用连着一百个工具，当前只需要「查代码」。可以先找到相关工具，再把完整参数结构交给模型。</p><p>这里延迟加载的可能是文档、Schema、模块、连接或进程。Schema 尚未进入模型上下文时，服务器可能早就连接好了。工具目录缓存命中，也不能证明某次调用一定能成功。</p><h2 id="mcp">MCP，连接应用与外部能力</h2><p><strong>MCP，全称 Model Context Protocol，统一应用接入外部能力的方式。</strong></p><p><strong>Host</strong> 是承载 AI 交互的应用；<strong>Client</strong> 是 Host 中与 Server 通信的协议组件；<strong>Server</strong> 暴露能力。它们是软件角色，不能直接推断进程数量。参见 <a href="https://modelcontextprotocol.io/docs/learn/architecture">MCP 官方架构说明</a>。</p><p>Server 可以提供三类核心内容。</p><table><thead><tr><th> 能力 </th><th> 用途 </th><th> 示例 </th></tr></thead><tbody><tr><td> Tools </td><td> 可执行操作 </td><td> 查询工单、创建记录 </td></tr><tr><td> Resources </td><td> 可读取的上下文资料 </td><td> 文档、数据库结构 </td></tr><tr><td> Prompts </td><td> 参数化提示模板 </td><td> 按固定维度分析资料 </td></tr></tbody></table><p><code>tools/list</code> 获取目录，<code>tools/call</code> 请求执行工具。应用常会把 MCP 工具转换为模型能理解的调用定义，再把模型请求路由到 Server。</p><p>MCP 不规定完整 Agent 的计划、长期记忆和验收策略。「接通 MCP」是接入成功的证据，业务权限和实际结果仍由对应系统负责。</p><h2 id="acpa2alspdap">ACP、A2A、LSP、DAP，连接对象不同</h2><table><thead><tr><th> 协议 </th><th> 连接谁 </th><th> 最直观的用途 </th></tr></thead><tbody><tr><td> ACP，Agent Client Protocol </td><td> 客户端与 Agent </td><td> 编辑器发任务，显示进度和权限请求 </td></tr><tr><td> A2A，Agent2Agent </td><td> 调用方与远程 Agent 服务 </td><td> 提交任务，跟踪状态，取交付物 </td></tr><tr><td> LSP，Language Server Protocol </td><td> 开发工具与语言服务 </td><td> 定义、引用、补全、诊断、重命名 </td></tr><tr><td> DAP，Debug Adapter Protocol </td><td> 开发工具与调试适配器 </td><td> 断点、单步、变量、调用栈 </td></tr></tbody></table><p>ACP 把会话创建、用户输入、更新和取消等交互定义为方法与通知。恢复会话等可选能力需要双方支持，不能只看「支持 ACP」就假设全部可用。<a href="https://agentclientprotocol.com/protocol/v1/overview">ACP v1 官方概览</a>列出了这些边界。</p><p>直接调用 CLI 是使用命令行入口；ACP 则提供结构化的客户端交互语义。Agent 可以同时支持二者，具体进程布局仍由实现决定。另有同名的 Agent Communication Protocol，看到 ACP 应先确认全称。</p><p>A2A 中的 <strong>Agent Card</strong> 描述身份、能力、地址和认证要求；<strong>Task</strong> 是带生命周期的任务；<strong>Message</strong> 是消息；<strong>Part</strong> 是其中的文本、文件或结构化内容单元；<strong>Artifact</strong> 是交付物。<a href="https://a2a-protocol.org/latest/specification/">官方规范</a>描述的是互操作，不替远程服务实现内部工具和记忆。</p><p>LSP 的例子更简单。搜索 <code>createOrder</code> 能找出同名文本，但不一定知道哪个引用指向哪个定义。语言服务利用代码语义提供这类关系，具体功能取决于语言服务器。<a href="https://microsoft.github.io/language-server-protocol/overview/">LSP 官方说明</a>可以继续阅读；运行过程的调试则属于 <a href="https://microsoft.github.io/debug-adapter-protocol/overview">DAP</a>。</p><h2 id="protocoltransportsdk">Protocol、Transport、SDK</h2><p><strong>Protocol 是消息格式和交互行为的约定。Transport 是消息怎么传。SDK 是帮助实现或调用接口的代码包。</strong></p><p><code>stdio</code> 使用进程标准输入输出；HTTP 承载请求响应；SSE 常用于服务端向客户端持续推送事件；WebSocket 支持持续双向通信。</p><p>同一传输可以承载不同协议，同一协议也可能支持多种传输。是否使用 HTTP，不能单独证明支持 MCP 或 A2A。</p><h2 id="code-mode">Code Mode，用程序组织工具</h2><p><strong>Code Mode 让模型先生成程序，再由执行环境运行程序，组织工具调用。</strong> 循环、条件、聚合和数据筛选可以在程序里完成。不同提供方的实现与命名不完全相同，<a href="https://developers.cloudflare.com/agents/api-reference/codemode/">Cloudflare 的 Code Mode 文档</a>是一种具体例子。</p><p>下面假定宿主已提供 <code>tools.listProjects</code> 和 <code>tools.listIssues</code>，只是编排示意。</p><pre class="language-js lang-js"><code class="language-js lang-js">const projects = await tools.listProjects();
const groups = await Promise.all(
  projects.map(p =&gt; tools.listIssues({ projectId: p.id }))
);
const result = groups.flat()
  .filter(issue =&gt; issue.priority === &quot;high&quot;)
  .map(issue =&gt; ({ title: issue.title, url: issue.url }));
</code></pre>
<p>程序查多个项目，筛出高优先级工单，再返回必要字段。模型不必为每个固定查询都参与一次往返判断，但实现还要考虑并发限制、失败和取消。</p><p><strong>PTC，Programmatic Tool Calling，程序化工具调用</strong>，描述相关思路，也可能特指某家托管实现。<strong>Sandbox</strong> 限制代码执行能力；<strong>Host Bridge</strong> 让执行环境中的代码调用宿主提供的受控能力。</p><p>脚本能调用工具之后，每次真实操作仍需要相应权限检查。沙箱限制本地文件访问，不会自动限制桥接工具在外部服务里能创建什么。</p><p><strong>Parallel Tool Calling</strong> 是一份模型响应请求多个工具，执行器决定怎样调度。普通工具执行器也可以并行，Code Mode 的特点在于用程序表达编排和处理逻辑。</p><p><strong>Tool Streaming</strong> 把调用信息或参数逐段返回。参数没收全、没校验之前，不能因为已经看到半段 JSON 就默认可执行。</p><h2 id="">把这一篇串起来</h2><p>代码审查 Skill 提供方法，MCP 接入仓库能力，Function Calling 表达读取请求，执行器返回代码；需要批量处理时，Code Mode 可以组织查询。编辑器通过 ACP 接收进度，LSP 提供符号关系。</p><p>下一篇再看外围程序怎样让这件事持续推进、保存状态，并在故障后恢复。</p><p><a href="https://binzi.top/2026/10/12/ai-agent-loop-runtime/">下一篇：Agent Loop、Runtime 与 Harness</a> · <a href="https://binzi.top/2026/10/12/ai-agent-concepts/">返回总览</a></p></div></div>]]></description><link>https://binzi.top/posts/ai/ai-agent-tools-protocols</link><guid isPermaLink="true">https://binzi.top/posts/ai/ai-agent-tools-protocols</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 12 Oct 2026 13:38:00 GMT</pubDate></item><item><title><![CDATA[Ai Agent概念扫盲：Agent Loop、ReAct、Runtime 与 Harness]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/ai-agent-loop-runtime">https://binzi.top/posts/ai/ai-agent-loop-runtime</a></blockquote><div><p><a href="https://binzi.top/2026/10/12/ai-agent-concepts/">返回总览</a> · 上一篇 <a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">工具与协议</a>。</p><p>用户让 Agent 修一个 Bug。它读日志、改代码、跑测试，失败后又继续改。这种过程看起来像模型一直在工作，实际上外围程序在不断组织调用、执行工具和更新状态。</p><p>这一篇沿着任务的推进与中断，把「循环」「运行」「恢复」放在一起解释。</p>
<h2 id="chatbotscriptworkflowagent">Chatbot、Script、Workflow、Agent</h2><p><strong>Chatbot</strong> 侧重输入与答复；<strong>Script</strong> 按程序逻辑执行；<strong>Workflow</strong> 预先安排步骤与分支；<strong>Agent</strong> 根据任务和反馈动态选择行动。</p><p>「读取文档 → 提取字段 → 校验 → 入库」可以是工作流。校验节点也能使用模型，但整体路径依然预先规定。</p><p>「排查登录失败」则可能先读代码，也可能先看日志，拿到新证据后再决定。<strong>Agentic</strong> 常用来描述这种参与决策和行动的特征，自主范围可以很小。</p><p>这是一种实用划分，实际系统经常混合使用。参考 <a href="https://www.anthropic.com/engineering/building-effective-agents">Anthropic 的工作流与 Agent 讨论</a>。</p><h2 id="agent-loop">Agent Loop，怎样转下一轮</h2><p><strong>Agent Loop 是围绕模型、行动和观察反复执行的控制过程。</strong></p><pre class="language-text lang-text"><code class="language-text lang-text">准备上下文 → 调模型 → 检查输出
                      ↓ 请求行动
                 校验并执行工具
                      ↓
                保存结果与更新状态
                      ↓
                  继续下一轮
</code></pre>
<p><strong>Policy</strong> 在这里指选择行动的机制；安全领域的 Policy 又常指约束规则，要看语境。<strong>Action</strong> 是行动；<strong>Observation</strong> 是得到的观察；<strong>Environment</strong> 是能观察或影响的环境；<strong>Trajectory</strong> 是行动和观察形成的轨迹。</p><p><strong>Step</strong> 是一步，但没有统一边界。有的实现算一次模型调用，有的算工具动作，有的算状态图节点。看到 <code>maxSteps = 10</code>，需要继续看它限制哪一种。</p><p>结束也有多种原因，完成、失败、取消、预算耗尽，或等待用户。不能把所有「停止」都展示成「任务完成」。</p><h2 id="react">ReAct，观察真的能改变下一步</h2><p><strong>ReAct 来自 Reasoning 和 Acting，强调推理与行动交替推进。</strong> <a href="https://arxiv.org/abs/2210.03629">原始论文</a>研究了二者结合的方式。</p><p>排查登录失败时，Agent 原先怀疑网络，读取日志后发现是数据库权限，再去检查连接配置。这次观察改变了原来的方向。</p><p>ReAct 不要求把完整内部思考展示给用户。用户更需要知道采取了哪些行动，得到什么证据，结论怎样形成。</p><p>前端开发中的 <strong>React</strong> 是 UI 库，与这里的 <strong>ReAct</strong> 分开理解。</p><h2 id="cotreflectionself-consistencytot">CoT、Reflection、Self-consistency、ToT</h2><p><strong>CoT，Chain of Thought，思维链</strong>，关注多步推理过程。<strong>Reflection</strong> 让模型评价自己的产出或行动。<strong>Self-consistency</strong> 采样多条推理路径，再利用最终答案的一致性选择结果。<strong>Tree of Thoughts，ToT</strong>，显式探索并评估多个候选思路。</p><p>这些研究方法都有具体机制，不是给提示词加一句「认真想想」就完整实现了。可以继续读 <a href="https://arxiv.org/abs/2201.11903">CoT</a>、<a href="https://arxiv.org/abs/2203.11171">Self-consistency</a> 和 <a href="https://arxiv.org/abs/2305.10601">ToT</a> 原始论文。</p><p>还有一个边界，模型自我评价不能代替独立验证。「这段代码应该正确」是一种判断；测试真正通过、目标业务行为真正出现，是另一种证据。</p><p><strong>Planner</strong> 拆计划，<strong>Executor</strong> 执行，<strong>Replanner</strong> 根据新证据修订，<strong>Critic</strong> 提出问题，<strong>Verifier</strong> 检查验收条件。这些角色可以由一个模型在不同阶段承担，也可以由不同组件承担，不必都做成独立 Agent。</p><h2 id="engineering-loop--event-loop">Engineering Loop 和 Event Loop</h2><p><strong>Engineering Loop，工程闭环</strong>，从要求出发，实施变更，取得真实反馈，根据反馈修正，再交付。</p><p>修登录 Bug 时，验收条件可以是「正确凭据能登录，错误凭据被拒绝，既有登录方式仍正常」。只改代码没有检查，反馈就不完整。</p><p><strong>Event Loop，事件循环</strong>，是运行时调度异步事件和回调的机制，例如 <a href="https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick">Node.js 的事件循环</a>。<code>for</code>、<code>while</code> 是语言中的循环控制结构。</p><p>Agent Loop 控制任务，Event Loop 调度异步程序，Engineering Loop 用证据修正实现。一个程序可以同时包含它们。</p><h2 id="runtimeharnessframeworksdk">Runtime、Harness、Framework、SDK</h2><p><strong>Runtime</strong> 有几个常见含义。语言 Runtime 是 Node.js、Python 等运行环境；Agent Runtime 承载循环、工具执行、状态和生命周期；与 Build-time 对比时，Runtime 指运行期间。</p><p><strong>Harness</strong> 是围绕模型建立的执行与反馈机制，可能包含指令、接口、上下文处理、权限、调度、恢复和退出条件。与 Runtime 的范围有重叠，不应把本篇分组当成强制架构标准。<a href="https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents">长任务 Harness 的官方案例</a>讨论了跨上下文窗口保持进展的问题。</p><p><strong>Harness Engineering</strong> 就是在设计这些外围机制。例如日志太长怎样分页、工具失败怎样反馈、中断后怎样继续、完成怎样判定。</p><p><strong>Framework</strong> 提供组织应用的方式和可复用组件；<strong>SDK</strong> 提供接口封装、类型与辅助代码。SDK 可能只封装模型请求，也可能包含 Agent Loop；是否支持权限、持久化和恢复，应分别确认。</p><table><thead><tr><th> 工程方向 </th><th> 主要调整什么 </th></tr></thead><tbody><tr><td> Prompt Engineering </td><td> 任务指令、示例、约束和输出要求 </td></tr><tr><td> Context Engineering </td><td> 每次调用看到哪些信息 </td></tr><tr><td> Tool / Interface Engineering </td><td> 工具参数、结果和错误表达 </td></tr><tr><td> Harness Engineering </td><td> 执行、状态、权限、恢复和完成判定 </td></tr><tr><td> Agent Engineering </td><td> 模型、工具、信息、执行和产品的整体配合 </td></tr><tr><td> Evaluation Engineering </td><td> 任务集、判分方法和证据 </td></tr><tr><td> LLMOps / AgentOps </td><td> 部署、监控、版本、成本和故障管理 </td></tr></tbody></table><h2 id="sessionrunprocess-">Session、Run、Process 不是一个对象</h2><p><strong>Conversation</strong> 是对话记录；<strong>Session</strong> 是带标识的交互或执行上下文；<strong>Turn</strong> 是一轮交互，边界依系统定义；<strong>Run</strong> 是一次运行实例；<strong>Task</strong> 是有目标和范围的工作；<strong>Attempt / Trial</strong> 是一次尝试。</p><p><strong>Process</strong> 是操作系统进程。<strong>Thread</strong> 既可能指操作系统线程，也可能指产品里的对话线程。</p><p>一个进程可以服务多个 Session，一个 Run 可以启动多个工具进程。界面关了以后任务是否继续，取决于后端生命周期，不取决于界面还保不保留聊天记录。</p><p><strong>Worker</strong> 是执行单元，<strong>Queue</strong> 存放待执行任务，<strong>Scheduler</strong> 决定何时交给谁，<strong>Daemon</strong> 是后台常驻进程。<strong>IPC</strong> 是进程间通信，<strong>PTY</strong> 是支持终端交互的伪终端。</p><p><strong>Concurrency，并发</strong>，多项工作在同一时间范围内推进；<strong>Parallelism，并行</strong>，强调真正同时执行。界面里出现三个任务，不能单独证明它们正在并行调用模型。</p><h2 id="state--source-of-truth">State 和 Source of Truth</h2><p><strong>State 是现在处于什么阶段。Source of Truth 是判断某类事实时应以哪份记录为准。</strong></p><p>任务可以处于排队、运行、等待输入、完成、失败或取消。文件是否修改，以工作区内容为准；工单是否创建，以工单系统为准。聊天里一句「已经创建」不能覆盖外部系统事实。</p><p><strong>Persistence，持久化</strong>，让进程退出后数据仍可读取。<strong>Checkpoint，检查点</strong>，保存从某个边界恢复所需的状态。<strong>Snapshot，快照</strong>，保存某时刻某范围的状态。</p><p><strong>Event Log</strong> 按序记录事件；<strong>Event Sourcing</strong> 以领域事件序列作为状态变化的权威记录。保存一些运行日志，不自动等于采用了 Event Sourcing。</p><p><strong>Replay</strong> 根据事件或已记录结果重建执行；<strong>Resume</strong> 从可恢复边界继续；<strong>Reconnect</strong> 恢复通信连接；<strong>Durable Execution</strong> 通过持久化、调度和恢复机制跨故障继续工作。</p><p>保存聊天历史不等于保存工具进度，检查点也不能保证从任意一行代码继续。<a href="https://docs.langchain.com/oss/python/langgraph/durable-execution">LangGraph 的持久执行文档</a>说明了恢复边界和副作用处理的重要性。</p><h2 id="">超时后为什么不能直接再来一次</h2><p><strong>Timeout</strong> 说明在限定时间内没有收到完成结果。它不能证明外部操作没有成功。</p><p>比如 Agent 请求创建工单，服务端创建成功，响应途中断线。直接再创建，就可能得到两张工单。</p><p><strong>Retry</strong> 是重试；<strong>Backoff</strong> 在重试间增加等待；<strong>Jitter</strong> 为等待加入随机变化，避免大量请求同时重试。<strong>Cancellation</strong> 请求停止，已经发生的外部效果不一定能撤销。</p><p><strong>Partial Success</strong> 表示一组操作只有部分成功；<strong>Compensation</strong> 用新的动作抵消已有部分效果，比如取消一条已经创建的预约，补偿本身也可能失败。</p><h2 id="idempotency--reconciliation">Idempotency 和 Reconciliation</h2><p><strong>Idempotency，幂等性</strong>，指在约定范围内重复同一请求，其预期业务效果与执行一次相同。它不要求每次返回文本完全一样，附带日志也可能增加。<a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.2">HTTP 规范的幂等性定义</a>可以作为一个基础例子。</p><p><strong>Idempotency Key</strong> 帮助服务识别重复请求。需要明确键的作用域、有效期、参数冲突和并发规则。</p><p><strong>Reconciliation</strong> 是状态核对。响应丢失后，先用业务标识查询工单是否已存在，再决定继续或重试。</p><table><thead><tr><th> 交付语义 </th><th> 在约定范围内的含义 </th></tr></thead><tbody><tr><td> At-most-once </td><td> 至多一次，可能没有发生 </td></tr><tr><td> At-least-once </td><td> 在约定故障假设下至少一次，可能重复 </td></tr><tr><td> Exactly-once </td><td> 恰好一次，必须说明覆盖哪些系统和边界 </td></tr></tbody></table><p>只有重试循环，不能自动得到 At-least-once 保证；本地数据库事务，也不能自动回滚已经发出的外部操作。</p><p><strong>Lock</strong> 互斥访问，<strong>Lease</strong> 在有效时间内授予执行权，<strong>Heartbeat</strong> 报告执行者还在活动，<strong>Version / CAS</strong> 通过版本比较检测并发修改，<strong>Transaction</strong> 在限定范围内保证一致性，<strong>Backpressure</strong> 在下游来不及时限制上游。</p><p>心跳说明仍有活动，不说明任务一定有进展。</p><h2 id="-agent-">看一个 Agent 运行系统，先找四个答案</h2><p>谁决定下一步，谁执行真实动作，状态存在哪里，怎样证明目标完成。</p><p>这四个答案清楚以后，再看 Runtime、Harness、Checkpoint，名词就能对应到具体代码。想继续沿真实实现读，可以看 <a href="https://binzi.top/2026/08/04/Pi%20Agent%20loop/">Pi Agent Loop</a>；里面的 Turn 等边界属于该实现，不能直接当成全行业定义。</p><p><a href="https://binzi.top/2026/10/12/ai-agent-context-memory/">下一篇：上下文、历史和记忆</a> · <a href="https://binzi.top/2026/10/12/ai-agent-concepts/">返回总览</a></p></div></div>]]></description><link>https://binzi.top/posts/ai/ai-agent-loop-runtime</link><guid isPermaLink="true">https://binzi.top/posts/ai/ai-agent-loop-runtime</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 12 Oct 2026 13:37:00 GMT</pubDate></item><item><title><![CDATA[Ai Agent概念扫盲：Prompt、Context、History 与 Memory]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/ai-agent-context-memory">https://binzi.top/posts/ai/ai-agent-context-memory</a></blockquote><div><p><a href="https://binzi.top/2026/10/12/ai-agent-concepts/">返回总览</a> · 上一篇 <a href="https://binzi.top/2026/10/12/ai-agent-loop-runtime/">循环与运行</a>。</p><p>你明明告诉过 Agent「这个项目不要新增依赖」，下一次它还是装了新包。</p><p>这时先不要急着认为模型没有记忆。那句话可能还在聊天历史里，只是没有进入本次模型输入；也可能被压缩成摘要时遗漏了。</p><p>理解记忆，要同时看信息保存在哪、怎样取出来、这次有没有交给模型。</p>
<h2 id="">七个词，先各放一个例子</h2><table><thead><tr><th> 名称 </th><th> 关注什么 </th><th> 例子 </th></tr></thead><tbody><tr><td> Prompt </td><td> 当前指令与输入，有时泛指整份模型输入 </td><td> 「修复登录 Bug，不要新增依赖」 </td></tr><tr><td> Context </td><td> 本次模型调用实际可用的信息 </td><td> 当前问题、相关历史、代码和工具定义 </td></tr><tr><td> History </td><td> 过去的交互记录 </td><td> 上个月的聊天和工具结果 </td></tr><tr><td> State </td><td> 当前执行状态 </td><td> 测试失败，等待修复；某工单已创建 </td></tr><tr><td> Memory </td><td> 为后续使用保存的信息 </td><td> 用户偏好、项目约束、历史经验 </td></tr><tr><td> Knowledge Base </td><td> 可检索的外部知识资料集合 </td><td> 接口文档、产品手册 </td></tr><tr><td> Cache </td><td> 为复用与性能保存的状态或结果 </td><td> 某次检索结果、嵌入向量 </td></tr></tbody></table><p>它们会互相转换。历史里可以提炼出记忆，记忆可以被取出放进上下文，工具结果可以保存为缓存。</p><p>「不要新增依赖」是约束；「已经修改 <code>login.ts</code>，测试尚未运行」是任务进度。压缩时需要保留两者，但保存目的不同。</p><h2 id="context-engineering">Context Engineering，每次到底给模型什么</h2><p><strong>Context Engineering，上下文工程，管理哪些信息、在什么时候、以什么形式进入模型。</strong></p><p>本次模型输入可能包括系统规则、用户目标、最近对话、工具定义、工具结果、检索资料和记忆。界面显示的内容、数据库保存的内容、模型实际收到的内容，不一定相同。</p><p>例如任务是修登录 Bug，可以先给模型文件路径和日志摘要，再按需要读取具体代码。如果一开始把整个仓库和所有日志都放进去，增加的既有资源开销，也有无关信息。</p><p>上下文的选择与按需读取，参见 <a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents">Anthropic 的上下文工程文章</a>。</p><h2 id="">历史太长时，有哪些处理方法</h2><p><strong>Selection</strong> 选择当前相关内容；<strong>Trimming</strong> 移除低价值或过期内容；<strong>Sliding Window</strong> 保留最近一段历史。它们可以很简单，但不能盲目删除更早的关键约束。</p><p><strong>Summarization</strong> 做摘要；<strong>Compaction</strong> 把长上下文压缩成继续工作所需的材料。摘要可以服务于阅读，Compaction 更关注任务能否接着做。具体实现也可能把两者合并。</p><p><strong>Offloading</strong> 把大量内容放到文件或外部存储；<strong>Just-in-time Retrieval</strong> 需要时再取；<strong>Context Packing</strong> 在预算内选择、去重和排列证据。</p><p>比如测试输出有十万行，可以把完整日志存到文件，给模型失败摘要和读取入口。存到文件之后，若没有留下路径和读取方法，后续模型仍然拿不到它。</p><h2 id="compaction-">Compaction 应该留下什么</h2><p>设想新的一次模型调用只拿到了摘要。它至少应当知道，当前目标、用户约束、已作决定、完成与未完成事项、验证结果、关键对象标识以及恢复位置。</p><p>一个为解释结构而写的示例如下。</p><pre class="language-text lang-text"><code class="language-text lang-text">目标：修复登录失败，不新增依赖。
已确认：错误来自刷新 Token 的分支。
已修改：src/login.ts 的 refreshSession。
验证：类型检查通过，登录集成测试未运行。
下一步：运行集成测试，检查过期 Token 场景。
注意：修改不涉及注册流程。
</code></pre>
<p>「研究了登录问题，进展顺利」读起来更短，却不能指导继续工作。</p><p>压缩仍然可能丢失细节。关键事实要能回到原文核查，实际文件和测试状态也要重新确认，摘要不能覆盖已经变化的环境。</p><p>已有的 <a href="https://binzi.top/2026/08/02/Pi%20Agent%20%E4%B8%8A%E4%B8%8B%E6%96%87%E5%B7%A5%E7%A8%8B/">Pi Agent 上下文工程</a>从真实实现解释了工具截断、项目规则和历史压缩。</p><h2 id="overflowpollutionrot">Overflow、Pollution、Rot</h2><p><strong>Context Overflow</strong> 是超出容量或请求限制。<strong>Context Pollution</strong> 是无关、错误或冲突信息进入上下文。<strong>Context Rot</strong> 常用来描述信息变长、变杂之后利用质量下降的现象，不是一个统一指标。</p><p>比如日志末尾显示数据库权限错误，旧摘要却说「已确定网络故障」，两条信息冲突，可能让模型继续沿过时假设行动。</p><p><strong>Lost in the Middle</strong> 是关于信息位置影响的研究，讨论了部分模型在部分任务中，中间位置的信息更难利用。不能从这项研究推出所有新模型都有同样的下降曲线；可以阅读 <a href="https://arxiv.org/abs/2307.03172">原始论文</a>，具体系统还应自己测。</p><h2 id="memory">Memory，两种分类不要混成一种</h2><p><strong>Short-term Memory</strong> 通常指当前任务或会话中的短期信息，<strong>Long-term Memory</strong> 跨任务或会话保留，<strong>Working Memory</strong> 是当前处理任务时使用的工作信息。边界依实现定义。</p><p>另一个维度按内容分类。</p><table><thead><tr><th> 类型 </th><th> 保存什么 </th><th> 示例 </th></tr></thead><tbody><tr><td> Semantic Memory，语义记忆 </td><td> 事实、偏好 </td><td> 用户希望用中文；项目使用 PostgreSQL </td></tr><tr><td> Episodic Memory，情景记忆 </td><td> 事件、经验 </td><td> 上次部署失败是环境变量缺失 </td></tr><tr><td> Procedural Memory，程序性记忆 </td><td> 方法、规则 </td><td> 发布前先检查迁移，再执行部署 </td></tr></tbody></table><p>一条长期记忆可以是事实，也可以是方法。这些分类帮助设计使用方式，不规定必须对应三个数据库表。<a href="https://docs.langchain.com/oss/python/langgraph/memory">LangGraph 的记忆文档</a>使用了相关分类。</p><h2 id="">记忆不一定要用向量数据库</h2><p>文件可以存项目约束，关系数据库可以存带来源和版本的事实，向量索引可以辅助查找相似经历，图结构可以组织实体关系。</p><p>小项目只有几条约束时，直接读取一个文件就可能足够。更多历史经验需要按任务查找时，再考虑相应检索方式。</p><p>「使用向量库」描述存储与检索选择，「有记忆」描述信息怎样被保留和再使用。二者不是等号。</p><h2 id="">记忆有写入、更新和遗忘</h2><p>一条记忆至少要考虑来源、作用域和时效。</p><p>用户说「这个仓库暂时不要升级 Node」，作用域是当前仓库，时间上可能只是暂时限制。若直接变成「用户永远不升级 Node」，就扩大了解读。</p><p><strong>Write</strong> 决定什么值得保存；<strong>Extraction</strong> 从记录中提炼；<strong>Merge / Update</strong> 处理新旧信息；<strong>Retrieval</strong> 取出当前需要的内容；<strong>Scope / Namespace</strong> 限定用户、项目或任务范围；<strong>Provenance</strong> 保留来源；<strong>Expiration / Forgetting</strong> 处理过期和遗忘。</p><p>新旧记录冲突时，应该查时间和来源，必要时向用户确认。临时猜测不能不加标记地写成长期事实。</p><p><strong>Hot-path Write</strong> 在当前请求中写入，较及时但增加前台工作；<strong>Background Write</strong> 后台提炼，前台负担较小，但最新记忆可能尚未可用。两种方式仍需避免重复、冲突和作用域串线。</p><h2 id="memory--rag-">Memory 和 RAG 怎样合作</h2><p>Memory 关注后续保留什么，RAG 关注回答时如何检索外部资料。</p><p>检索上次部署经验，可能同时涉及 Memory 和 RAG；检索产品手册，可以只有 RAG；读取固定的用户偏好文件，可以有 Memory，但没有复杂检索。</p><p>判断「Agent 为什么忘了」时，可以顺着查，原始记录是否存在、是否提炼、作用域是否匹配、是否取出、是否进入本次上下文、内容是否过期或冲突。这样才能知道要修存储、检索，还是输入组织。</p><p><a href="https://binzi.top/2026/10/12/ai-agent-rag/">下一篇：RAG 的完整链路</a> · <a href="https://binzi.top/2026/10/12/ai-agent-concepts/">返回总览</a></p></div></div>]]></description><link>https://binzi.top/posts/ai/ai-agent-context-memory</link><guid isPermaLink="true">https://binzi.top/posts/ai/ai-agent-context-memory</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 12 Oct 2026 13:36:00 GMT</pubDate></item><item><title><![CDATA[Ai Agent概念扫盲：一篇串起 RAG、Embedding、BM25、RRF 与重排]]></title><description><![CDATA[<link rel="preload" as="image" href="https://binzi.top/images/ai-agent-concepts/rag-pipeline.svg"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/ai-agent-rag">https://binzi.top/posts/ai/ai-agent-rag</a></blockquote><div><p><a href="https://binzi.top/2026/10/12/ai-agent-concepts/">返回总览</a> · 上一篇 <a href="https://binzi.top/2026/10/12/ai-agent-context-memory/">上下文与记忆</a>。</p><p>你问「今年出差住宿费最多报销多少」，模型训练时学到的知识通常回答不了你公司的最新规定。</p><p>系统可以先找到公司的报销文件，再用文件里的规定回答。<strong>这种检索外部资料、结合资料生成回答的方式，就是 RAG，Retrieval-Augmented Generation，检索增强生成。</strong></p><p>这篇用一个构造的报销查询例子，把 RAG 的概念顺着链路讲完。检索多少条、分块多大，都只是示例，不是推荐参数。</p>
<h2 id="rag-">RAG 有资料准备和查询回答两条线</h2><p><img src="https://binzi.top/images/ai-agent-concepts/rag-pipeline.svg" alt="RAG 的资料准备与查询回答：权限和版本贯穿检索，候选经过融合、重排与证据组织" height="0" width="0"/></p><p>资料准备，把文档变成能检索的内容。查询回答，找到证据并生成答案。</p><p>2020 年的 <a href="https://arxiv.org/abs/2005.11401">RAG 原始论文</a>研究了模型与检索记忆结合的具体方法；现在应用开发里常用更宽的含义，未必复制原论文架构。搜索引擎、SQL、文件检索也可以参与，不是只有「向量数据库加聊天框」一种形式。</p><h2 id="ingestion">Ingestion，先把资料读对</h2><p><strong>Ingestion 是资料接入和准备。Data Source 是来源，Loader / Connector 负责读取。Parsing 是解析。</strong></p><p>PDF 可能包含双栏、表格、脚注和扫描件。<strong>OCR</strong> 识别图片中的文字；<strong>Layout Analysis</strong> 识别版面结构；<strong>Cleaning</strong> 清理噪声；<strong>Deduplication</strong> 去重。</p><p>如果「北京 500 元、其他城市 350 元」被解析成错列，后面的检索和回答可能完全忠实，却忠实于一份错误内容。</p><p><strong>Metadata</strong> 是来源、标题、版本、时间、权限等附加信息。<strong>Document ID / Chunk ID</strong> 标识文档和片段；<strong>Provenance</strong> 保留可追溯来源；<strong>ACL</strong> 表示访问控制信息。</p><p>报销文件更新后，<strong>Incremental Indexing</strong> 只更新变化内容，<strong>Reindexing</strong> 重新建立索引。如何识别旧版本、处理删除以及保证权限同步，属于整个资料生命周期。</p><p>文档分块与资料结构可继续参考 <a href="https://learn.microsoft.com/en-us/azure/search/vector-search-how-to-chunk-documents">Azure AI Search 的分块说明</a>。</p><h2 id="chunking">Chunking，切块时别切断意思</h2><p><strong>Chunk 是检索和上下文组织使用的内容单元。Chunking 是把资料切成这些单元。</strong></p><p>报销文档十几页，问题只涉及住宿。整份交给模型可能浪费，按主题切成片段，就能更精准地取出相关章节。</p><table><thead><tr><th> 方法 </th><th> 怎样切 </th><th> 常见取舍 </th></tr></thead><tbody><tr><td> Fixed-size </td><td> 按固定长度 </td><td> 简单，但可能切断句子或表格 </td></tr><tr><td> Recursive </td><td> 逐层按段落、句子等边界 </td><td> 尽量保留结构，仍需控制块大小 </td></tr><tr><td> Structure-aware </td><td> 按标题、章节、代码结构 </td><td> 依赖解析质量 </td></tr><tr><td> Semantic </td><td> 按语义变化 </td><td> 需要相应算法或模型，结果要评估 </td></tr><tr><td> Parent-child </td><td> 小块用于检索，再读取大父块 </td><td> 精确匹配后补上下文 </td></tr><tr><td> Sentence Window </td><td> 命中句子后补邻近句 </td><td> 补回指代和条件 </td></tr><tr><td> Overlap </td><td> 相邻块保留部分重叠 </td><td> 降低边界损失，增加重复 </td></tr></tbody></table><p>例如「500 元」被单独切出去，城市、日期和适用条件不在同一块，答案就容易出错。块太大，又可能混入机票、交通等无关规定。</p><p><strong>Late Chunking</strong> 则先让较长文本参与表示计算，再得到块向量，尝试保留跨块上下文。它改变嵌入阶段的处理，不能等同于简单增加最终返回片段长度。见 <a href="https://arxiv.org/abs/2409.04701">Late Chunking 论文</a>。</p><p>读取父块或邻近片段时，权限检查也要覆盖扩展出来的内容。</p><h2 id="embedding">Embedding，把内容变成可比较的表示</h2><p><strong>Embedding 是把文本、图片等对象转换为数值表示。Embedding Model 是生成这种表示的模型。</strong></p><pre class="language-text lang-text"><code class="language-text lang-text">「住宿费上限」 → [0.12, -0.48, 0.73, ...]
</code></pre>
<p>每一维通常不能直接解释成「住宿」或「金额」。检索主要比较整体表示之间的关系。</p><p><strong>Dense Vector，稠密向量</strong>，通常大多数分量非零；<strong>Sparse Vector，稀疏向量</strong>，大多数分量为零；<strong>Dimension</strong> 是分量数量；<strong>Norm</strong> 是向量长度；<strong>Normalization</strong> 按规则归一化。</p><p><strong>Bi-Encoder，双编码器</strong>，分别编码问题和文档，再比较表示。文档表示可以提前计算，查询时只需要编码新问题，适合大量候选的初步检索。</p><p>同一索引要使用兼容的表示空间。换了嵌入模型，不能只因向量维度一样，就默认旧向量仍可直接使用。</p><h2 id="bm25">BM25，关键词也很有用</h2><p><strong>Keyword Search 按词项匹配检索。Inverted Index，倒排索引，记录一个词出现在哪些文档或位置。</strong></p><p><strong>TF</strong> 是词频，<strong>DF</strong> 是包含该词的文档数，<strong>IDF</strong> 反映该词的区分度；<strong>Analyzer</strong> 处理分词、归一化等步骤。<strong>BM25</strong> 是常用词项相关性评分方法，考虑词频饱和与文档长度等因素。细节可读 <a href="https://nlp.stanford.edu/IR-book/html/htmledition/okapi-bm25-a-non-binary-model-1.html">Stanford 的 BM25 说明</a>。</p><p>「住酒店能报多少」和「住宿费标准」字面不同，语义检索可能有帮助。但文件编号 <code>FIN-2026-08</code>、错误码或函数名，精确词项匹配又很重要。</p><p>实际效果还受分词、数据和查询影响，不能固定认为向量检索一定优于关键词检索。</p><h2 id="cosinedot-productl2">Cosine、Dot Product、L2，怎样比较向量</h2><p><strong>Cosine Similarity</strong> 比较方向相似程度；<strong>Dot Product</strong> 计算点积；<strong>Euclidean / L2 Distance</strong> 比较欧氏距离。</p><p>向量单位归一化后，点积可以等于余弦相似度。采用哪一种，是否归一化，应该遵循模型与索引要求。</p><p>「取消订单流程」和「取消订单失败」可能很相似，但一个问操作步骤，一个问故障原因。向量相似高，不自动表示可以回答当前问题。</p><h2 id="ann">向量库、索引、ANN，主要解决效率</h2><p><strong>Vector Database</strong> 提供向量存储、索引、查询与过滤等能力；<strong>Vector Index</strong> 是用于搜索的数据结构。数据库还可以包含权限、元数据和其他查询功能，不能只看一个索引算法。</p><p><strong>kNN</strong> 找最近的 K 个邻居；<strong>Exact Search</strong> 在给定距离下做精确搜索；<strong>ANN</strong> 做近似最近邻搜索，用一定近似换速度和资源。</p><table><thead><tr><th> 方法 </th><th> 直观理解 </th></tr></thead><tbody><tr><td> HNSW </td><td> 在分层近邻图里逐步寻找候选 </td></tr><tr><td> IVF </td><td> 先划分区域，查询时重点搜索部分区域 </td></tr><tr><td> PQ，乘积量化 </td><td> 压缩表示，降低存储等资源需求 </td></tr></tbody></table><p>它们可能组合使用。效率、内存和近邻召回之间的取舍可以看 <a href="https://github.com/facebookresearch/faiss/wiki/Faiss-indexes">Faiss 官方索引说明</a>。</p><p>HNSW 的图表达近邻关系，知识图谱的图表达实体关系，两个「图」不应混淆。</p><h2 id="">查询表达不清楚，先处理问题</h2><p>用户说「今年这个还能报吗」，系统至少需要弄清哪个费用、哪个年份和哪个制度。</p><p><strong>Query Rewrite</strong> 改写查询；<strong>Query Expansion</strong> 扩展同义词或缩写；<strong>Multi-query</strong> 用多个查询覆盖不同表达；<strong>Query Decomposition</strong> 把复杂问题拆成子问题；<strong>Query Routing</strong> 决定查哪个来源；<strong>Multi-hop Retrieval</strong> 根据前一次结果继续检索。</p><p>例如问「北京住宿和机场交通合计最多报多少」，可能需要分别查两种标准，再核对是否有组合限制。改写不得悄悄改变用户的城市、日期或问题范围。</p><p><strong>HyDE，Hypothetical Document Embeddings</strong>，先生成假设性答案文本，再利用其向量检索真实资料。假设文本只帮助找资料，不能当证据。见 <a href="https://arxiv.org/abs/2212.10496">HyDE 原始论文</a>。</p><h2 id="top-kthreshold">召回、Top K、Threshold</h2><p><strong>Retrieval / Recall Stage，召回阶段</strong>，先找到可能相关的候选。这里「召回」表示一个处理阶段，不等于后面统计的 Recall 指标。</p><p><strong>Top K</strong> 取排名前 K 的结果；<strong>Threshold</strong> 按分数阈值筛选。关键词取前 30，向量取前 30，重排后取前 5，最后进入上下文 3 块，这些 K 可以不同。</p><p>分数能否当置信度、阈值怎样设，需要用可回答与不可回答的问题验证。相似度 0.8 不能自动解释为「80% 正确」。</p><h2 id="hybrid-searchfusionrrf">Hybrid Search、Fusion、RRF</h2><p><strong>Hybrid Search，混合检索</strong>，结合关键词和向量等多种方法。<strong>Fusion</strong> 把多路结果合并。</p><p><strong>Score Fusion</strong> 使用分数，必要时处理尺度差异；<strong>Rank Fusion</strong> 主要利用名次。<strong>RRF，Reciprocal Rank Fusion，倒数排名融合</strong>，是一种常见排名融合。</p><pre class="language-text lang-text"><code class="language-text lang-text">RRF(d) = 各路 1 / (k + 该文档在该路的排名) 之和
</code></pre>
<p>某篇文档在某一路没有出现，那一路不加分。这里的 <code>k</code> 是平滑常量，与「取前 K 条」的 K 不同。<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">Elasticsearch 的 RRF 文档</a>解释了公式和实现参数。</p><p>例如关键词列表是 A、B、C，向量列表是 B、C、A，B 在两路都靠前，融合后可能超过 A。融合利用排名信号，不直接判断文档中的内容是否真的回答了问题。</p><p>这部分已有一篇 <a href="https://binzi.top/2026/07/09/RRF%E6%8E%92%E5%BA%8F%E8%9E%8D%E5%90%88/">RRF 排序融合</a>，包含手算和 Python 示例，可以从这里继续往下看。</p><h2 id="rerankercross-encoder">Reranker、Cross-Encoder，把候选仔细比较</h2><p><strong>Rerank 是对已经召回的候选重新排序。Reranker 是执行重排的模型或组件。</strong></p><p><strong>Cross-Encoder，交叉编码器</strong>，联合处理问题与候选文本，给出相关性信号。初步检索用双编码器从大量资料找候选，之后用交叉编码器精细比较较少候选，是一种常见组合。<a href="https://sbert.net/examples/sentence_transformer/applications/retrieve_rerank/README.html">Sentence Transformers 的 Retrieve &amp; Re-Rank</a>给出了示例。</p><p>「酒店预订流程」可能和住宿问题很像，但「住宿报销上限」才回答金额。重排可以帮助区分，不过正确文件根本没进候选时，重排无法把它凭空找回来。</p><h2 id="mmrcontext-packing">MMR、Context Packing，挑哪些证据进去</h2><p><strong>MMR，最大边际相关性</strong>，在相关性与多样性之间平衡，减少重复候选。<strong>Context Packing</strong> 在上下文预算内选择、去重和排列证据。</p><p>如果前五块全是同一段住宿标准的副本，应该补充适用日期、地区和例外条件，而不是把五份重复内容全部送给模型。</p><p>这一步可能再次丢掉信息。检索系统找到了正确文档，不代表最终 Context 中仍保留了那条关键限制。</p><h2 id="groundingcitationabstention">Grounding、Citation、Abstention</h2><p><strong>Grounding</strong> 让具体说法由证据支持；<strong>Citation</strong> 标明来源；<strong>Abstention</strong> 在依据不足时明确无法确认；<strong>Hallucination</strong> 是生成与事实或可用依据不符的内容。</p><p>回答「北京住宿上限 500 元」时，引用至少要支持城市、金额、适用时间和人群。链接存在只证明能打开资料，不证明资料支持这句话。</p><p>如果只找到旧制度，就说明日期限制，继续找新资料，或给出无法确认。更完整的错误治理可以读已有的 <a href="https://binzi.top/2026/04/10/%E6%80%8E%E4%B9%88%E5%87%8F%E5%B0%91%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%B9%BB%E8%A7%89/">怎么减少大模型幻觉？</a>。</p><h2 id="graphrag">GraphRAG，用实体关系补充检索</h2><p><strong>Knowledge Graph，知识图谱</strong>，用实体与关系组织信息。<strong>Entity</strong> 是实体，<strong>Relation</strong> 是关系，<strong>Attribute</strong> 是属性，<strong>Triple</strong> 是「主体—关系—客体」。</p><p>例如「财务部—发布—报销制度」。<strong>Entity Resolution</strong> 判断不同名字是否指同一个对象；<strong>Ontology / Schema</strong> 定义类型与关系规则；<strong>Graph Traversal</strong> 沿关系查找；<strong>Community</strong> 是联系较紧密的一组节点。</p><p>广义 <strong>GraphRAG</strong> 是利用图结构辅助检索生成的一类方法。微软 GraphRAG 则是具体系统，涉及实体关系和社区报告等索引。使用图数据库，不等于实现了微软 GraphRAG。</p><p>微软的 <strong>Local Search</strong> 结合实体相关图信息与原文；<strong>Global Search</strong> 利用社区报告处理整体性问题；<strong>DRIFT</strong> 结合社区信息与后续查询扩展。具体方式见 <a href="https://microsoft.github.io/graphrag/query/overview/">官方查询概览</a>。</p><p>问某项住宿上限，直接定位原文就可能足够；问一组制度总体有哪些主题，则是另一种检索需求。图结构有成本，也可能抽错关系，应按问题验证收益。</p><h2 id="agentic-ragcorrective-ragself-rag">Agentic RAG、Corrective RAG、Self-RAG</h2><p><strong>Agentic RAG</strong> 让 Agent 决定是否检索、查哪里、怎样改写、是否继续，以及证据是否足够。查到旧制度后主动寻找新版本，是一个简单例子。它是一类组织方式，具体实现差异很大。</p><p><strong>Corrective RAG</strong> 围绕检索质量评价和纠正组织流程，相关原论文包含检索评价、知识处理与外部搜索等机制，见 <a href="https://arxiv.org/abs/2401.15884">CRAG 论文</a>。</p><p><strong>Self-RAG</strong> 原论文包含训练和反思 Token 等设计。给普通提示词加上「检查一下资料」，不能直接称为完整实现 Self-RAG。见 <a href="https://arxiv.org/abs/2310.11511">Self-RAG 原始论文</a>。</p><p>名字里的 Agentic、Corrective、Self 描述关注方向，不保证质量更好。新增调用可能增加延迟，新增评价模型也可能判断错误。</p><h2 id="colbertlate-interaction-">ColBERT、Late Interaction 和其他方向</h2><p><strong>Late Interaction，后期交互</strong>，分别形成较细粒度表示，再匹配。<strong>ColBERT</strong> 是代表方法之一，保留 Token 级表示，与每段只用一个向量、或问题和文档一起进入 Cross-Encoder 都不同。见 <a href="https://arxiv.org/abs/2004.12832">ColBERT 论文</a>。</p><p><strong>Multimodal RAG</strong> 检索和利用图片、表格、音频等资料。<strong>Text-to-SQL</strong> 把自然语言问题转为数据库查询。<strong>Structured Retrieval</strong> 使用字段过滤、SQL 或图查询取得资料。</p><p>「上季度各部门实际报销多少」可能适合查结构化数据库；「新制度怎样规定」可能适合文档检索。并非所有问题都应改写成向量搜索。</p><h2 id="">评测先看检索，再看回答</h2><p>某问题共有 4 条相关资料，系统前 5 条结果找回其中 2 条，这是一个构造例子。</p><table><thead><tr><th> 指标 </th><th> 衡量什么 </th><th> 示例 </th></tr></thead><tbody><tr><td> Precision@K </td><td> 前 K 条里相关结果的比例 </td><td> Precision@5 = 2/5 = 40% </td></tr><tr><td> Recall@K </td><td> 全部相关资料中找回的比例 </td><td> Recall@5 = 2/4 = 50% </td></tr><tr><td> Hit@K </td><td> 前 K 条是否有至少一个相关结果 </td><td> 当前查询 Hit@5 = 1，多查询可汇总比例 </td></tr><tr><td> MRR </td><td> 第一个相关结果有多靠前 </td><td> 第 2 名命中，该查询倒数排名为 1/2 </td></tr><tr><td> MAP </td><td> 多个相关结果的排名表现 </td><td> 对查询的平均精确率再做平均 </td></tr><tr><td> nDCG </td><td> 考虑位置与不同相关程度 </td><td> 高度相关资料排前面通常更好 </td></tr><tr><td> ANN Recall </td><td> 找回多少精确近邻 </td><td> 比较近似结果与精确向量搜索结果 </td></tr></tbody></table><p>ANN Recall 衡量近邻找回，相关性 Recall 衡量回答依据找回，两者不一样。指标还需说明标签和未标注结果怎样处理，参见 <a href="https://nlp.stanford.edu/IR-book/html/htmledition/evaluation-of-ranked-retrieval-results-1.html">Stanford 的检索评测说明</a>。</p><p>回答层的 <strong>Answer Correctness</strong> 看正确性，<strong>Faithfulness</strong> 看是否忠实于给定材料，<strong>Answer Relevance</strong> 看是否回答问题，<strong>Context Precision / Recall</strong> 看上下文相关程度和覆盖，<strong>Citation Accuracy</strong> 看引用是否支持声明。</p><p>忠实于错误旧制度的回答仍然可能错。同名指标在不同框架中实现也可能不同，例如 <a href="https://docs.ragas.io/en/stable/concepts/metrics/available_metrics/context_precision/">Ragas 的 Context Precision</a>考虑了排名等因素，不能直接等同于普通 Precision@K。</p><h2 id="">回答错了，沿这张表找原因</h2><table><thead><tr><th> 现象 </th><th> 优先检查 </th></tr></thead><tbody><tr><td> 新制度根本搜不到 </td><td> 接入、解析、版本、权限、索引更新 </td></tr><tr><td> 正确制度排得很后 </td><td> 查询表达、召回、融合和重排 </td></tr><tr><td> 检索到了，模型没有看到 </td><td> 去重、截断、Context Packing </td></tr><tr><td> 上下文正确，回答还是错 </td><td> 指令、证据利用和生成 </td></tr><tr><td> 引用存在，结论无依据 </td><td> 片段定位、归因和推断边界 </td></tr><tr><td> 资料已更新，仍答旧标准 </td><td> 索引和缓存失效、数据版本 </td></tr></tbody></table><p>RAG 的重点是把需要的依据找出来、送到模型面前，并正确使用。先检查这条链路，再决定要不要加更复杂的方法。</p><p><a href="https://binzi.top/2026/07/09/RRF%E6%8E%92%E5%BA%8F%E8%9E%8D%E5%90%88/">深入：RRF 计算与代码</a> · <a href="https://binzi.top/2026/04/10/%E6%80%8E%E4%B9%88%E5%87%8F%E5%B0%91%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%B9%BB%E8%A7%89/">深入：减少幻觉</a> · <a href="https://binzi.top/2026/10/12/ai-agent-collaboration-computer-use/">下一篇：多 Agent 与电脑操作</a></p></div></div>]]></description><link>https://binzi.top/posts/ai/ai-agent-rag</link><guid isPermaLink="true">https://binzi.top/posts/ai/ai-agent-rag</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 12 Oct 2026 13:35:00 GMT</pubDate></item><item><title><![CDATA[Ai Agent概念扫盲：多 Agent、Handoff、LSP 与 Computer Use]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/ai-agent-collaboration-computer-use">https://binzi.top/posts/ai/ai-agent-collaboration-computer-use</a></blockquote><div><p><a href="https://binzi.top/2026/10/12/ai-agent-concepts/">返回总览</a> · 上一篇 <a href="https://binzi.top/2026/10/12/ai-agent-rag/">RAG</a>。</p><p>一个 Agent 修代码，另一个检查是否破坏旧功能，看起来像两个人配合。</p><p>但它们是否看到相同资料、是否共享目录、谁最后负责合并结果，都需要程序设计。多 Agent 不会因为数量增加就自动形成团队。</p><p>这一篇把协作和观察环境放在一起讲，它们共同影响任务怎样从「安排了」走向「真的完成」。</p>
<h2 id="multi-agent--subagent">Multi-agent 和 Subagent</h2><p><strong>Multi-agent 是多个 Agent 协作的系统。Subagent 是被委派子任务的 Agent。</strong></p><p>子 Agent 可能拿到自己的目标、上下文和工具，也可能与主 Agent 共享部分环境。它可以使用同一个模型服务，独立 Agent 不等于独立模型或独立进程。</p><p>例如主 Agent 把任务拆成「修复」「检查兼容性」「验证回归」。每个子任务应有明确的范围、输入和交付标准。否则几名 Agent 都在读同一份代码、改同一个文件，成本和冲突就会增加。</p><h2 id="agent-as-a-tool--handoff">Agent-as-a-Tool 和 Handoff</h2><p><strong>Agent-as-a-Tool</strong> 把另一个 Agent 当成可调用能力。主 Agent 发出请求，收到结果，再决定下一步。</p><p><strong>Handoff</strong> 把控制权移交给另一个 Agent，例如通用客服把后续退款处理交给退款 Agent。用户接下来主要与接手方交互，具体形式依实现定义。</p><p>「让审查 Agent 返回报告」和「让退款 Agent 接管后续对话」是两种交互语义。工具调用返回之后，主 Agent 通常仍保留控制；交接需要明确谁继续负责。</p><p>协作模式可参考 <a href="https://docs.langchain.com/oss/python/langchain/multi-agent">LangChain 的多 Agent 文档</a>。这些是设计模式，不是各家同名功能完全一致的保证。</p><h2 id="">常见编排方式</h2><p><strong>Orchestration，编排</strong>，安排任务依赖、调度、信息传递和汇总。</p><table><thead><tr><th> 模式 </th><th> 怎样工作 </th><th> 例子 </th></tr></thead><tbody><tr><td> Supervisor / Worker </td><td> 主 Agent 分配，Worker 执行 </td><td> 主 Agent 安排修复与审查 </td></tr><tr><td> Fan-out / Fan-in </td><td> 分发独立任务，再收集 </td><td> 分析三个模块后汇总 </td></tr><tr><td> Map / Reduce </td><td> 对多个对象分别处理，再聚合 </td><td> 分别概括文件，再总结仓库 </td></tr><tr><td> Debate / Critique </td><td> 提出或检查不同方案 </td><td> 方案比较与独立审查 </td></tr><tr><td> Shared Blackboard </td><td> 通过共享状态交换信息 </td><td> 各参与者更新共同任务板 </td></tr><tr><td> DAG </td><td> 用无环依赖图安排任务 </td><td> 编译后才能跑某些测试 </td></tr><tr><td> State Graph </td><td> 用状态与转换组织执行，可含循环 </td><td> 测试失败后回到修复 </td></tr></tbody></table><p>DAG 不包含环，State Graph 可以包含。把任务画成图，只说明依赖被表达出来，持久化和失败恢复仍需要额外实现。</p><h2 id="">什么工作适合分出去</h2><p>相对独立的子任务更容易并行。比如一个检查登录逻辑，一个检查文档，两者修改范围不同，最终可以一起汇总。</p><p>先读代码才能确定修改点，先修改才能验证，这些步骤有依赖，不能直接同时推进。</p><p>交给子 Agent 的输入应保留目标、约束、上下文入口和交付标准。主 Agent 汇总时，还要处理结果冲突，不能把几份报告拼起来就默认成立。</p><p>共享工作区时，需要明确文件责任范围。隔离工作区时，也需要合并和重新验证集成结果。各自通过测试，不保证合在一起仍通过。</p><p>多 Agent 的收益可能是不同资料范围、独立审查和并行时间；代价包括重复上下文、交接损失、额外调用、取消传播和协调。相似模型看到相似资料时，也可能给出相同错误，投票数量不能自动变成事实证据。</p><h2 id="-agent-">编码 Agent 怎样观察代码</h2><p><strong>Text Search</strong> 查字符串和关键词，适合错误码、配置名或特定文案。<strong>LSP</strong> 提供定义、引用、类型和诊断等语言服务。</p><p>例如 <code>createOrder</code> 在两个模块里同名，文本搜索都能找出来；语言服务能帮助确定某处引用属于哪一个定义。LSP 能力依语言服务器而定，调用成功也要检查结果是否覆盖当前文件版本。</p><p><strong>AST，Abstract Syntax Tree，抽象语法树</strong>，表达代码的语法结构，适合一些结构化分析和变换。它与简单字符串匹配不同，但具体工具是否理解类型、能否安全重构，要继续看实现。</p><h2 id="diffpatch">Diff、Patch、字符串替换和整文件重写</h2><p><strong>Diff</strong> 表示两个版本的差异；<strong>Patch</strong> 描述要应用的变更；<strong>String Replacement</strong> 匹配旧文本并替换；<strong>Full Rewrite</strong> 重写整个文件。</p><p>字符串替换直观，但依赖准确匹配；Patch 适合表达局部差异，但需要足够上下文定位；AST 修改适合部分结构变换；整文件重写容易表达完整结果，但改动范围更大。</p><p>选择编辑方式之后，还要读回结果。成功返回说明编辑器做完了某次操作，不说明它命中了正确业务位置，也不说明项目仍能运行。</p><p>已有的 <a href="https://binzi.top/2026/10/10/kimi-code-edit-design/">Kimi Code 与 Coding Agent 的 Edit 设计</a>比较了具体实现。版本结论以那篇记录的源码范围为准。</p><h2 id="aci">ACI，模型与电脑之间的接口</h2><p><strong>ACI，Agent-Computer Interface，是 Agent 与计算机交互的接口设计。</strong></p><p>同样是搜索工具，返回十万行混杂文本，和返回带文件路径、行号、分页入口的结果，对后续判断的帮助不同。</p><p>接口还需要说明文件是否截断、错误发生在哪里、修改失败如何恢复、怎样确认操作生效。<a href="https://arxiv.org/abs/2405.15793">SWE-agent 原始论文</a>把这类接口设计作为自动软件工程的重要部分。</p><h2 id="computer-use">Computer Use，怎样观察和操作界面</h2><p><strong>Computer Use</strong> 让系统通过工具观察和操作电脑。浏览器操作也可以使用类似的观察、行动和反馈链。</p><table><thead><tr><th> 方式 </th><th> 提供什么 </th><th> 需要注意什么 </th></tr></thead><tbody><tr><td> 业务 API </td><td> 直接读写业务对象 </td><td> 认证、权限、版本和真实结果 </td></tr><tr><td> DOM </td><td> 网页元素结构和内容 </td><td> 结构不等于最终业务状态 </td></tr><tr><td> Accessibility Tree </td><td> 可访问性语义，如按钮名称和状态 </td><td> 标注可能缺失，观察可能过期 </td></tr><tr><td> Screenshot </td><td> 实际画面 </td><td> 图像识别、坐标和窗口状态 </td></tr><tr><td> 鼠标键盘 </td><td> 点击、输入、快捷键 </td><td> 焦点、目标与后续校验 </td></tr></tbody></table><p><strong>Accessibility Tree，可访问性树</strong>，把界面中的语义角色、标签和状态提供给辅助技术，也能用于工具定位。<strong>AX Tree</strong> 是一些环境中的相关简称。</p><p>截图能显示 DOM 中不容易表达的画面，却需要识别坐标；语义结构能帮助准确找到按钮，却也可能缺失或落后于页面。选哪种观察方式，要看能回答当前问题的证据。</p><h2 id="locatorselectorauto-wait">Locator、Selector、Auto-wait</h2><p><strong>Locator</strong> 描述怎样找到目标元素，例如名称为「保存」的按钮。<strong>Selector</strong> 是某种具体选择表达式，如 CSS 或 XPath。</p><p><strong>Actionability</strong> 指元素是否适合操作，例如可见、启用、没有被遮挡；<strong>Auto-wait</strong> 自动等待相关条件满足。它们解决的是操作条件，不替代业务验收。可继续看 <a href="https://playwright.dev/docs/locators">Playwright 定位器文档</a>和 <a href="https://playwright.dev/docs/actionability">自动等待说明</a>。</p><p><strong>Navigation</strong> 是页面跳转，<strong>Dialog</strong> 是对话框，<strong>Download / Upload</strong> 是下载与上传；浏览器中的 <strong>Session / Cookies</strong> 参与保存登录和交互状态，不能和 Agent 的 Session 直接混用。</p><h2 id="">点击成功之后，再看结果</h2><p>点保存之后，页面可能出现校验失败；填写成功之后，网络可能未提交；提交成功之后，后台处理可能还没完成。</p><p>保存资料应确认成功状态或重新读取对象，下载应确认取得文件，创建工单应确认正确对象存在。界面动作、网络响应和业务结果是不同的观察层。</p><p>这与多 Agent 汇总是同一个原则，安排出去的任务，要拿回能够核验的结果。</p><p><a href="https://binzi.top/2026/10/12/ai-agent-cache-observability/">下一篇：缓存、Trace 与性能</a> · <a href="https://binzi.top/2026/10/12/ai-agent-concepts/">返回总览</a></p></div></div>]]></description><link>https://binzi.top/posts/ai/ai-agent-collaboration-computer-use</link><guid isPermaLink="true">https://binzi.top/posts/ai/ai-agent-collaboration-computer-use</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 12 Oct 2026 13:34:00 GMT</pubDate></item><item><title><![CDATA[Ai Agent概念扫盲：Cache Hit、KV Cache、Prompt Cache 与性能]]></title><description><![CDATA[<link rel="preload" as="image" href="https://binzi.top/images/ai-agent-concepts/prompt-cache.svg"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/ai-agent-cache-observability">https://binzi.top/posts/ai/ai-agent-cache-observability</a></blockquote><div><p><a href="https://binzi.top/2026/10/12/ai-agent-concepts/">返回总览</a> · 上一篇 <a href="https://binzi.top/2026/10/12/ai-agent-collaboration-computer-use/">协作与电脑操作</a>。</p><p>Agent 面板显示「缓存命中」，你却还是等了很久。</p><p>这不一定是缓存没用。命中的可能只是工具目录，而耗时主要发生在模型输出或外部查询。要理解性能，先弄清复用了什么，再看整条任务链的时间。</p>
<h2 id="cache-hit">Cache Hit，找到了满足复用条件的缓存</h2><p><strong>Cache 是为复用与性能保存的计算状态或结果。Cache Hit 是命中，Cache Miss 是未找到可用缓存。</strong></p><p>「有缓存」还不够，至少需要说明缓存对象。</p><table><thead><tr><th> 类型 </th><th> 缓存什么 </th><th> 简单例子 </th></tr></thead><tbody><tr><td> KV Cache </td><td> 注意力计算中的 Key、Value 状态 </td><td> 生成后续 Token 时复用已有计算 </td></tr><tr><td> Prompt Cache </td><td> 可复用输入前缀的处理结果或计算状态 </td><td> 稳定指令和长文档前缀 </td></tr><tr><td> Response Cache </td><td> 完整回答 </td><td> 直接复用相同问题的旧答案 </td></tr><tr><td> Tool Result Cache </td><td> 工具执行结果 </td><td> 复用一次文档读取 </td></tr><tr><td> Embedding Cache </td><td> 已计算向量 </td><td> 相同文本不重复编码 </td></tr><tr><td> Retrieval Cache </td><td> 查询与检索结果 </td><td> 复用某版本知识库的候选 </td></tr><tr><td> Semantic Cache </td><td> 对语义近似请求复用结果 </td><td> 类似问题匹配已有答案 </td></tr><tr><td> Tool Catalog Cache </td><td> 工具名称、描述、Schema 等目录 </td><td> 不重新获取工具列表 </td></tr></tbody></table><p>这些缓存可以同时存在，但命中其中一种不证明其他环节也命中。</p><h2 id="kv-cache">KV Cache，不是用户记忆数据库</h2><p><strong>KV Cache 保存的是注意力计算中的 Key 和 Value 表示。</strong> 自回归模型生成后续 Token 时，可以复用先前 Token 的部分计算。</p><p>这里的 Key、Value 是模型计算里的表示，不是「用户名 → 用户偏好」那种键值存储。KV Cache 也不能直接替代长期记忆。</p><p>具体原理可以看 <a href="https://huggingface.co/docs/transformers/cache_explanation">Hugging Face 的缓存说明</a>。缓存状态需要资源，长度与模型结构等也会影响内存需求，不能把计算复用理解为完全没有开销。</p><h2 id="prompt-cache">Prompt Cache，重复计算减少，信息还在</h2><p><strong>Prompt Cache 通常复用符合条件的稳定输入前缀。</strong> 具体条件、最小长度、有效时间、计费和统计依提供方定义，不能跨产品直接套用。</p><p>假设有 8,000 Token 稳定前缀，本次追加 2,000 Token，前缀满足复用条件。这是解释概念的例子。</p><p><img src="https://binzi.top/images/ai-agent-concepts/prompt-cache.svg" alt="缓存复用 8,000 Token 前缀计算，逻辑输入仍约 10,000 Token，输出重新生成" height="0" width="0"/></p><p>模型本次逻辑输入仍约有 10,000 Token。复用的是计算，并没有把前缀内容从上下文容量中删除；模型通常还要生成新输出，也可能继续调用工具。</p><p>如果缓存直接返回完整旧答案，那是 Response Cache 的场景。<a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching">Claude 的 Prompt caching 文档</a>说明了其具体规则，本篇不把这些规则推广成通用保证。</p><h2 id="keyttlevictioninvalidation">Key、TTL、Eviction、Invalidation</h2><p><strong>Cache Key</strong> 决定用什么标识查找缓存；<strong>TTL</strong> 是有效时间；<strong>Eviction</strong> 因容量或策略移除缓存；<strong>Invalidation</strong> 因资料、权限或版本变化让缓存失效。</p><p>例如报销制度更新，旧检索结果虽然还在 TTL 内，也可能已经不该复用。用户失去访问权限后，缓存不能继续把旧资料返回给他。</p><p>缓存键可能需要纳入用户或租户、数据版本、模型版本、查询条件等信息，具体取决于缓存对象。敏感结果跨用户复用前，更要明确作用域。</p><p><strong>Hit Rate</strong> 是命中比例，但统计口径必须说明。按请求计，按 Token 计，只统计可缓存请求，得到的百分比都可能不同。</p><p>工具目录命中，只说明复用目录信息，不证明服务仍可达、Schema 仍最新或权限仍有效。</p><h2 id="semantic-cache">Semantic Cache，相似请求要考虑业务差异</h2><p>「今年北京住宿标准」和「去年北京住宿标准」语义很像，答案却可能不同。「我的余额」不能因为文字相同就跨用户复用。</p><p>语义缓存要判断哪些差异允许复用，哪些条件必须严格匹配。相似度阈值也不能直接当答案正确概率。</p><p>动态工具定义可能改变输入前缀，但影响依具体接口设计而定。不能只凭「增加了工具」就断言一定完全破坏缓存，也不能认为目录缓存能保证模型前缀命中。</p><h2 id="observability">Observability，知道哪里发生了什么</h2><p><strong>Observability，可观测性</strong>，帮助解释系统运行情况。常用信号包括 Logs、Metrics 和 Traces。</p><table><thead><tr><th> 信号 </th><th> 作用 </th><th> 示例 </th></tr></thead><tbody><tr><td> Logs </td><td> 离散事件记录 </td><td> 重排失败、工具超时 </td></tr><tr><td> Metrics </td><td> 数量与分布 </td><td> 成功率、队列长度、耗时分位数 </td></tr><tr><td> Trace </td><td> 关联一次过程的多个操作 </td><td> 一次任务经过模型、检索和工具 </td></tr><tr><td> Span </td><td> 一个操作的起止、状态和属性 </td><td> 某次检索用了多少时间 </td></tr><tr><td> Event </td><td> 某一时刻的变化 </td><td> 用户取消、工具完成 </td></tr><tr><td> Correlation ID </td><td> 关联分散记录 </td><td> 客户端请求与后台 Run 对应 </td></tr></tbody></table><p>Trace 可以跨进程，Span 可以嵌套；详细定义见 <a href="https://opentelemetry.io/docs/concepts/signals/">OpenTelemetry 的信号说明</a>。</p><p>日志帮助解释故障，检查点和恢复机制才帮助继续执行。只有详细日志，不会自动获得可靠恢复能力。</p><h2 id="latencyttfttpot">Latency、TTFT、TPOT，起点终点要说清</h2><p><strong>Latency</strong> 是从指定起点到终点的延迟。<strong>TTFT</strong> 是首 Token 延迟；<strong>TPOT</strong> 是生成阶段平均每个 Token 的时间，具体采样和统计口径需要说明；<strong>End-to-end Latency</strong> 是任务从提交到完成的总时间。</p><p>首字一秒出现，但后面还要查十个外部服务，任务仍可能很慢。</p><p><strong>Throughput</strong> 是单位时间处理的请求、Token 或任务数量；<strong>Concurrency</strong> 是同一时间范围内处理中的任务数量。吞吐增加不保证每个请求的延迟下降。</p><p><strong>p50、p95、p99</strong> 是延迟分位数。p95 为 10 秒，通常表示约 95% 的观测值不超过该时间，需要同时说明统计窗口和样本范围。</p><p><strong>RPM</strong> 是每分钟请求数，<strong>TPM</strong> 是每分钟 Token 数，常用于服务限额或速率描述。</p><h2 id="">时间还可能花在哪里</h2><p>排队、读取文件、模型处理输入、输出生成、工具网络、数据库写入、外部任务等待和界面渲染，都可能计入用户等待。</p><p><strong>CPU / GPU</strong> 是计算资源；<strong>RAM / VRAM</strong> 是主内存和显存；<strong>I/O</strong> 是文件、网络、数据库等输入输出。</p><p><strong>Cold Start</strong> 是首次启动或重新准备环境的开销；<strong>Warm Pool</strong> 保留预热资源；<strong>Connection Pool</strong> 复用连接；<strong>Worker Pool</strong> 复用执行单元。</p><p><strong>Batching</strong> 合并多项工作；<strong>Continuous Batching</strong> 在模型服务中动态安排生成请求；<strong>Speculative Decoding</strong> 通过草稿候选与目标模型验证等方式减少部分生成开销。这些属于服务与推理技术，不等于增加多个 Agent，收益也需要实测。</p><h2 id="rate-limitingquota-">Rate Limiting、Quota 和运行保护</h2><p><strong>Rate Limiting</strong> 限制调用速率；<strong>Quota</strong> 限制总用量或资源；<strong>Backpressure</strong> 在下游来不及时限制上游；<strong>Circuit Breaker</strong> 在依赖连续异常时暂时阻止继续请求。</p><p><strong>Fallback</strong> 切换替代方案；<strong>Degradation</strong> 接受部分功能或质量降低；<strong>Model Routing</strong> 按任务选择模型；<strong>Cascade</strong> 先走一条路径，再按条件升级。</p><p>比如便宜模型先分类，无法可靠判断时升级到更强模型，是一种 Cascade。但错误的分类可能使请求根本没有升级，最终效果要看任务成功率、质量和总成本。</p><h2 id="">优化之前，先建立可比较的记录</h2><p>「少了工具定义字符」可以证明输入规模减少；「缓存命中」可以证明某些计算复用；「首字更快」可以证明对应指标改善。要证明整个任务更快，需要看端到端耗时和成功率。</p><p>同样一批任务、相近环境，记录模型调用、工具耗时、缓存口径与结果，再决定优化哪一层。若工具等待占了大部分时间，单独调整提示词未必能带来明显收益。</p><p><a href="https://binzi.top/2026/10/12/ai-agent-safety-evaluation/">下一篇：权限、测试与评测</a> · <a href="https://binzi.top/2026/10/12/ai-agent-concepts/">返回总览</a></p></div></div>]]></description><link>https://binzi.top/posts/ai/ai-agent-cache-observability</link><guid isPermaLink="true">https://binzi.top/posts/ai/ai-agent-cache-observability</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 12 Oct 2026 13:33:00 GMT</pubDate></item><item><title><![CDATA[Ai Agent概念扫盲：权限、信任边界、Test、Eval 与工程交付]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/ai-agent-safety-evaluation">https://binzi.top/posts/ai/ai-agent-safety-evaluation</a></blockquote><div><p><a href="https://binzi.top/2026/10/12/ai-agent-concepts/">返回总览</a> · 上一篇 <a href="https://binzi.top/2026/10/12/ai-agent-cache-observability/">缓存与性能</a>。</p><p>Agent 说「已经修好了」，你真正关心的是问题是否消失，旧功能有没有受影响。</p><p>同样，工具已经连上，不代表能做任何操作；模型认为可以做，也不能代替权限检查。这一篇回答两个问题，有没有权做，怎样知道做对了。</p>
<h2 id="authnauthzapprovalsandbox">AuthN、AuthZ、Approval、Sandbox</h2><table><thead><tr><th> 概念 </th><th> 回答什么 </th><th> 例子 </th></tr></thead><tbody><tr><td> Authentication / AuthN，认证 </td><td> 你是谁 </td><td> 识别登录账户 </td></tr><tr><td> Authorization / AuthZ，授权 </td><td> 你能做什么 </td><td> 账户可读工单，但不可删除 </td></tr><tr><td> Approval，单次批准 </td><td> 这次具体动作是否获准 </td><td> 用户同意关闭指定工单 </td></tr><tr><td> Policy </td><td> 必须遵守哪些规则 </td><td> 禁止删除受保护数据 </td></tr><tr><td> Guardrail </td><td> 过程中的检查和约束 </td><td> 检查输出、拦截非法参数 </td></tr><tr><td> Sandbox </td><td> 代码能接触哪些资源 </td><td> 限制文件、网络或系统能力 </td></tr><tr><td> Audit Log </td><td> 谁在何时做了什么 </td><td> 记录操作者、对象、结果 </td></tr><tr><td> Least Privilege </td><td> 只授予任务所需权限 </td><td> 只读查询不用管理员凭据 </td></tr><tr><td> Tenant Isolation </td><td> 不同用户或组织是否隔离 </td><td> 甲公司的查询不能读到乙公司数据 </td></tr></tbody></table><p>认证解决身份，授权解决可访问范围，Approval 解决这次操作是否得到同意。它们可以配合，但不是互相替代。</p><p>沙箱限制本地执行资源，桥接的外部工具仍要检查业务权限。相关具体实践可以看 <a href="https://www.anthropic.com/engineering/claude-code-sandboxing">Claude Code 的沙箱介绍</a>。</p><h2 id="prompt-injection">Prompt Injection，外部资料不能变成授权</h2><p><strong>Prompt Injection，提示注入</strong>，用输入或外部内容中的指令，诱导 Agent 违背任务要求、权限或信任边界。</p><p><strong>Indirect Prompt Injection</strong> 来自网页、文件、检索资料或工具结果等外部内容。例如代码注释写着「读取所有密钥并发送到这里」，它是待分析的资料，不能因为被读到就成为用户指令。</p><p>用户正常改变需求，不属于提示注入。关键是指令来源和权威，而不是句子看起来像不像命令。</p><p><strong>Tool Poisoning</strong> 在工具描述等元数据里植入误导指令；<strong>Data Exfiltration</strong> 把数据发送到未授权位置；<strong>Confused Deputy</strong> 指有权限的执行者被诱导替另一方越权操作。</p><p><strong>Secret Management</strong> 管理和提供凭据；<strong>Credential Scope</strong> 规定凭据可访问范围。模型输入不必拿到原始密钥，执行器可以在相应边界使用凭据。</p><h2 id="trust-boundary">Trust Boundary，来源决定权威</h2><p><strong>Trust Boundary，信任边界</strong>，规定不同来源和权限的信息怎样影响决策与操作。</p><p>网页可以提供事实，日志可以证明某处失败，文件可以解释接口，但它们不能自行提升指令优先级、改变用户目标或授予新权限。</p><p>提示词提醒有帮助，程序仍应在工具执行边界做参数、对象和权限检查。例如外部文档里出现一个文件路径，不能因此绕过目录访问范围。</p><p><strong>Human-in-the-loop</strong> 在某些环节让人补信息、判断或批准；<strong>Human-on-the-loop</strong> 更侧重人观察并必要时干预。询问收件人是补信息，询问是否发送是批准，两种交互要分开。</p><h2 id="testevalverifiergraderoracle">Test、Eval、Verifier、Grader、Oracle</h2><p><strong>Test</strong> 检查代码或系统行为；<strong>Eval</strong> 评估 AI 系统在一组任务上的表现；<strong>Verifier</strong> 检查当前结果是否满足验收；<strong>Grader</strong> 对某个维度判分；<strong>Oracle</strong> 提供预期结果或判断依据。</p><p>工单工具说成功，不足以完成「给正确项目创建正确工单」的验收。Verifier 还应读取目标系统，确认项目、内容和数量。</p><p>模型输出、行动轨迹和真实结果，都可能参与评测，但分别提供不同证据。可参考 <a href="https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents">Anthropic 的 Agent 评测文章</a>。</p><h2 id="">常见软件测试，用场景理解</h2><table><thead><tr><th> 类型 </th><th> 检查什么 </th><th> 登录任务中的例子 </th></tr></thead><tbody><tr><td> Unit Test </td><td> 某个模块或行为 </td><td> Token 过期判断 </td></tr><tr><td> Integration Test </td><td> 多个组件配合 </td><td> 认证服务与数据库 </td></tr><tr><td> Contract Test </td><td> 接口双方约定 </td><td> 返回字段与类型 </td></tr><tr><td> E2E Test </td><td> 从入口到结果 </td><td> 打开页面、登录、进入账户 </td></tr><tr><td> Smoke Test </td><td> 核心路径是否基本可用 </td><td> 服务能启动、能完成基本登录 </td></tr><tr><td> Regression Test </td><td> 既有行为是否被破坏 </td><td> 旧登录方式仍可用 </td></tr><tr><td> Property-based Test </td><td> 多组输入下的不变量 </td><td> 过期 Token 始终不能被接受 </td></tr><tr><td> Fault Injection </td><td> 人为制造故障 </td><td> 响应丢失、断线、重启 </td></tr><tr><td> Mutation Testing </td><td> 测试能否发现刻意改坏的逻辑 </td><td> 反转判断后测试是否失败 </td></tr><tr><td> Load Test </td><td> 压力与并发 </td><td> 大量登录请求下的行为 </td></tr></tbody></table><p><strong>Fixture</strong> 准备测试数据和环境；<strong>Assertion</strong> 是必须成立的条件；<strong>Mock / Stub / Fake</strong> 是不同测试替身，框架用法也有差异；<strong>Coverage</strong> 看执行到多少代码或分支；<strong>Flaky Test</strong> 是代码没变仍时过时不过的测试。</p><p>覆盖率高不保证断言有效，使用模拟数据库也不能证明生产数据库正常。测试通过时，需要同时说明测试范围。</p><h2 id="ai-">AI 评测怎样判分</h2><p><strong>Code-based Grader</strong> 用程序和规则判分，适合对象存在、数量、格式等可验证条件。<strong>LLM-as-a-Judge</strong> 用模型按标准评价，适合某些开放性质量。<strong>Human Evaluation</strong> 由人或专家评估。</p><p><strong>Rubric</strong> 明确评价维度；<strong>Pairwise Comparison</strong> 比较两份结果；<strong>Calibration</strong> 检查自动评分与可靠参考的一致性。</p><p>模型判分也可能受到篇幅、措辞或顺序影响。能直接验证的业务条件，应保留直接证据；开放评价则要检查评分是否稳定、是否与人工判断一致。</p><p><strong>pass@k</strong> 常描述允许 K 次尝试时至少一次成功的能力；<strong>pass^k</strong> 在一些评测中描述连续 K 次都成功的可靠性。具体计算以评测定义为准。</p><p>一个构造例子，假设每次独立成功概率为 80%，三次至少成功一次的概率是 <code>1 - 0.2³ = 99.2%</code>，三次全成功则为 <code>0.8³ = 51.2%</code>。独立性只是这个示例的假设，真实尝试可能相关，不能拿公式代替实际评测。</p><h2 id="baselineablationleakage">Baseline、Ablation、Leakage</h2><p><strong>Baseline</strong> 是比较基线；<strong>Ablation，消融</strong>，去掉或替换某机制来检查贡献；<strong>A/B Test</strong> 在可比较条件下对照方案；<strong>Controlled Variable</strong> 是控制变量。</p><p>想知道 Reranker 是否有用，可以在同一查询集比较有无重排，而不要同时更换嵌入模型、分块和提示词，否则难以归因。</p><p><strong>Leakage，泄漏</strong>，是系统看到了本不该看到的信息，比如评测答案进入提示。<strong>Overfitting，过拟合</strong>，过度适应已见题目，未见任务表现下降。<strong>Held-out Set</strong> 是留出的测试集。</p><p>不断针对同十道题改提示词，也可能过拟合，测试集应覆盖真正会遇到的变化。</p><h2 id="prdspec">PRD、Spec、验收标准，把任务写清楚</h2><p><strong>PRD</strong> 描述用户、问题、范围和目标；<strong>Spec</strong> 描述预期行为、接口和约束。<strong>SDD</strong> 常指规格驱动开发，也可能指软件设计文档，需看全称。</p><p><strong>Acceptance Criteria</strong> 是任务验收标准；<strong>Definition of Done</strong> 是团队共同的完成要求。登录任务的验收可以是正确凭据通过、错误凭据拒绝、旧流程兼容；完成要求还可能包括审查、文档和发布准备。</p><p><strong>TDD</strong> 先写暴露需求缺口的测试，再实现和重构；<strong>BDD</strong> 用业务场景描述行为；<strong>ADR</strong> 记录架构决策和取舍；<strong>RFC</strong> 提出方案供讨论。</p><p><strong>AGENTS.md、CLAUDE.md</strong> 等文件承载项目或工具说明，加载范围、顺序和优先级依宿主实现，不能只凭文件名推断所有产品行为相同。它们与 Skill 可以协作。</p><h2 id="commitprcideployment">Commit、PR、CI、Deployment</h2><p><strong>Commit</strong> 保存一组版本变更；<strong>Branch</strong> 是独立演进路径；<strong>Worktree</strong> 是同一仓库的额外工作目录；<strong>PR / MR</strong> 提议把改动合入目标分支；<strong>Merge</strong> 合并版本变化；<strong>Rollback</strong> 回退版本或状态。</p><p>分支隔离 Git 历史，Worktree 提供额外目录，外部数据库或服务未必随之隔离。不能把「在另一个分支运行」理解成所有副作用都安全隔离。</p><p><strong>CI，持续集成</strong>，频繁集成并用构建、测试验证；<strong>Continuous Delivery，持续交付</strong>，保持可发布状态，生产发布可以人工决定；<strong>Continuous Deployment，持续部署</strong>，通过门槛后自动部署生产。</p><p>它们的区别可读 <a href="https://continuousdelivery.com/">持续交付的定义</a>。代码已提交、CI 已通过、版本已部署、真实用户问题已解决，也需要分别确认。</p><p><strong>SLI</strong> 是衡量服务的指标，<strong>SLO</strong> 是目标，<strong>SLA</strong> 是服务级别协议。例如请求成功率是指标，对成功率设目标，再形成相应服务协议，属于不同层次。</p><h2 id="todotaskgoaltask-runtask-graph">Todo、Task、Goal、Task Run、Task Graph</h2><p><strong>Todo</strong> 跟踪小事项；<strong>Task</strong> 是有目标和范围的工作；<strong>Goal</strong> 可能需要多个任务；<strong>Task Run</strong> 是某任务的一次执行；<strong>Task Graph</strong> 表达任务依赖。产品可能使用不同命名，要回到它的对象定义。</p><p>「修登录」可以是任务，「运行过期 Token 测试」是其中一个事项，「提高账户系统可靠性」是更长期目标。同一任务可能运行多次，每次结果和证据应对应自己的 Run。</p><h2 id="">最后把完成标准落到结果上</h2><p>一次修复的交付，应让读者看清改了什么、验证了哪些行为，以及还有哪些限制。一次外部操作，应检查目标对象真实状态。</p><p>模型说完成，是待验证的产出；真实结果符合要求，才有完成的依据。把这个习惯带回工具、RAG、协作和运行恢复里，整套概念就能连起来。</p><p><a href="https://binzi.top/2026/10/12/ai-agent-concepts/">回到总览，选择其他专题</a> · <a href="https://binzi.top/2026/10/12/ai-agent-model-basics/">从第一篇重新读：模型基础</a></p></div></div>]]></description><link>https://binzi.top/posts/ai/ai-agent-safety-evaluation</link><guid isPermaLink="true">https://binzi.top/posts/ai/ai-agent-safety-evaluation</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 12 Oct 2026 13:32:00 GMT</pubDate></item><item><title><![CDATA[我是怎么设计 Biny Agent 工具系统的]]></title><description><![CDATA[<link rel="preload" as="image" href="https://binzi.top/images/biny-tool-system/tool-discovery.svg"/><link rel="preload" as="image" href="https://binzi.top/images/biny-tool-system/execution-coordinator.svg"/><link rel="preload" as="image" href="https://binzi.top/images/biny-tool-system/file-editing.svg"/><link rel="preload" as="image" href="https://binzi.top/images/biny-tool-system/unknown-outcome.svg"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/biny-agent-tool-system">https://binzi.top/posts/ai/biny-agent-tool-system</a></blockquote><div><p>最近整理了一下 <a href="https://github.com/binyouo/Biny-Agent">Biny Agent</a> 的工具系统。</p><p>给 Agent 增加一个工具其实不难：定义参数 Schema，注册工具，再实现执行函数。但接入 MCP、插件和各种扩展以后，工具数量越来越多，问题就变了。</p><p>只读一个文件，有必要把上百个工具的定义都交给模型吗？缺少能力时，模型能不能自己找到工具？不同来源的调用，能不能共用一套权限和执行机制？如果一次写入中途断线，又该不该重试？</p><p>我现在更关心的是，<strong>工具从被发现到真正产生副作用，中间的每一步能不能说清楚。</strong></p><p>这篇文章就沿着这条链路，聊聊我在 Biny 中的实现和取舍。</p>
<h2 id="1-">1. 注册了工具，不代表模型每轮都要看到它</h2><p>我先区分了两个集合：<strong>系统拥有的工具</strong>，以及<strong>模型当前可用的工具</strong>。</p><p>在 Biny 中，内置工具以及 MCP、Plugin、Skill、Subagent 相关的工具入口，统一进入 <code>ToolRegistry</code>。注册表维护名称、描述、参数 Schema、来源和能力信息。</p><p>但注册只是说明系统知道这个工具，不意味着每一轮请求都要把它的完整定义放进模型上下文。Skill 的说明文档也有自己的加载流程，不能把它和工具 Schema 混为一谈。</p><p>假设接入几个 MCP Server，一共提供 100 个工具。用户只是想修改一个配置文件，此时把日历、邮件、GitHub 等工具的参数定义全部发给主模型，会产生很多与当前任务无关的输入。</p><p>所以我的处理方式是：工具目录保留完整能力，模型侧只开放当前需要的一部分。</p><p>自动工具选择模式下，会保留 <code>Read</code>、<code>Write</code>、<code>Edit</code>、<code>Bash</code> 等基础 Coding 能力，以及工具发现、结果回读等必要入口；其他工具按任务选择。具体如何呈现，还会随模型的工具交互模式调整。</p><p><strong>工具目录负责“有什么”，模型工具集负责“现在用什么”。</strong> 两者不需要始终一样。</p><h2 id="2-">2. 先预选，缺少能力时再搜索</h2><p>只给基础工具也有问题：主模型后来需要访问 GitHub，却没有相关工具，怎么办？</p><p>工具定义我延迟加载。一开始我只给主模型基础能力和这一轮用得上的那几条，完整参数不跟全目录一起塞进去。</p><h3 id="">请求开始前：我先预选</h3><p>自动选择时，我让辅助模型看当前请求和最近的对话，从目录里挑相关工具。它看到的是名称和短描述，不是每条工具的完整参数。</p><p>你要看某个 PR，它就可以先挑出相关的 GitHub 能力。你点名了某个有效工具，我在本地直接放进去，不靠它猜。</p><p>预选我设了超时和预算。失败了，我留下基础能力和还有效的历史能力，不退回把全部工具展开。</p><p>这个预算管的是<strong>这一轮新自动选进来的工具</strong>。已经在用的、必须留的，我不拿它去淘汰。所以它不是整个会话工具集的硬上限。</p><p>我拿同一批任务比过两条路。每回合都做辅助预选，8 个任务都做完。只留基础和历史工具、缺了再用 ToolSearch，做完 4 个。没做完的那一侧，常常一次搜索都没发出去。</p><p>预选更慢，也更贵。第一个主模型请求的中位延迟是 2.28 秒，另一侧是 0.106 秒。ToolSearch 次数是 0 对 2。主模型和辅助模型加起来，预选用了 95,982 token，按需搜索用了 65,312。后面这个数更低，是因为有一半任务没跑完，不能当成省下来的成本。</p><p>样本只有 8 个。我据此留下了每回合预选，没有按工具定义占窗口的比例自动切换。这个阈值还没做实验。</p><p>另外，固定留在主模型身边的只有读、写、改、命令、工具搜索和回读结果。其余工具留在注册表里，靠预选、搜索或你点名再进来。当时这组固定定义从 11,605 字降到 5,668 字。每次请求至少少带这一截工具说明。这是字符数，我没有把它折成 token，也没有单独测这一截省了多少延迟。</p><h3 id="">运行过程中：主动发现缺失能力</h3><p>预选只是我在回合开始前的预测。多步任务后面还会缺工具，所以我留了 <code>ToolSearch</code>。</p><p>它区分两种情况：</p><ul><li><strong>知道完整工具名：</strong> 从本地注册表精确查找。</li><li><strong>只知道要做什么：</strong> 把需求和候选工具的简短描述交给辅助模型，进行语义选择。</li></ul><p>辅助模型返回的名称还要和真实注册表核对。凭空生成一个工具名，不能让系统获得不存在的能力。</p><p>成功发现后，只把匹配工具开放给后续模型步骤，而不是顺带展开整个 MCP Server。</p><p><img src="https://binzi.top/images/biny-tool-system/tool-discovery.svg" alt="Biny 的工具按需发现流程：完整目录经过预选形成当前工具集，缺少能力时通过 ToolSearch 补充" height="0" width="0"/></p><p><em>图 1：目录可以很大，但每一步只需要披露当前相关的工具。发现成功后，下一步模型才能使用新增能力。</em></p><p>举个例子，用户先要求查看 PR，随后又要求读取关联 Issue。第一步预选未必包含 Issue 工具，主模型可以在执行过程中再搜索并补充它。</p><p>成功发现的工具还可以在会话后续回合中复用，前提是注册仍然有效。这样能减少重复搜索，也会带来累积问题：任务不断变化，工具集可能越来越大。</p><p>这是一项需要继续评测的取舍，而不是已经解决了所有上下文管理问题。</p><h2 id="3-mcp-">3. MCP 连接、工具发现和实际调用，要分开处理</h2><p>接入 MCP 后，我发现还有三个动作容易混在一起：<strong>连接服务、发现工具、执行工具。</strong></p><p>它们的状态并不相同。</p><table><thead><tr><th> 环节 </th><th> 回答的问题 </th><th> 不能据此推断什么 </th></tr></thead><tbody><tr><td> 连接 </td><td> 远端服务现在能否通信？ </td><td> 所有工具都应该进入上下文 </td></tr><tr><td> 发现 </td><td> 目录中有哪些相关能力？ </td><td> 缓存中的工具现在一定可执行 </td></tr><tr><td> 执行 </td><td> 这次调用能否获得结果？ </td><td> 发出请求就代表操作已完成 </td></tr></tbody></table><p>Biny 会在 Runtime 启动时异步连接启用的 MCP Server，并给连接准备设置等待预算。慢服务可以继续在后台连接，不需要让整个 Agent 无限等待。</p><p>成功获取的工具目录，会在同一 Runtime Host 内短期缓存。符合缓存条件的服务重连时，可以先利用已有目录进行发现，实际执行仍然需要等待连接就绪。</p><p><strong>缓存的元数据，不是服务在线的证明。</strong></p><p>真正调用时，要检查连接和工具定义。如果服务更新了 Schema，旧调用需要重新发现和准备，不能直接沿用。单个服务的连接异常也独立处理，尽量避免影响其他服务。</p><p>这里还需要分清两种收益：目录缓存主要减少重复发现的工作；减少主模型无关输入的，是工具预选和按需披露。</p><p>辅助模型本身也消耗 Token，并增加等待时间。主模型输入变小，只能说明其中一项成本下降，不能直接证明总成本更低。</p><h2 id="4-">4. 调用形式可以不同，执行入口必须统一</h2><p>模型发现一个工具，只代表它具备被调用的条件，不代表获得了执行权限。</p><p>我不希望内置文件工具、MCP 工具和脚本组合调用各自维护一套权限逻辑。来源越多，分散的执行边界就越难核对。</p><p>因此，Biny 用 <code>ToolExecutionCoordinator</code> 协调工具调用。把主要步骤简化后，大致是：</p><p><strong>参数校验 → 执行准备 → 权限判断 → 资源调度 → 目标复核 → 执行与记录 → 返回结果。</strong></p><p>执行准备阶段会确定真实路径、操作目标、风险和资源访问范围。权限判断尽量绑定具体操作，例如修改哪个文件、修改什么内容，而不只是批准一个工具名称。</p><p>调度层根据工具声明的访问范围处理冲突。涉及文件修改时，执行前还会复核目标，避免审批之后文件已经变化，却继续按旧预览写入。</p><p><img src="https://binzi.top/images/biny-tool-system/execution-coordinator.svg" alt="不同来源的工具调用经过统一执行协调器，依次处理参数、目标、权限、调度、执行与记录"/></p><p><em>图 2：这是主要执行步骤的概念图。状态记录贯穿调用过程，并不只在工具结束时发生。</em></p><p>Code Mode 我做成可选，默认还是一条一条直接调工具。打开之后，模型写一段 JavaScript，我放在 QuickJS 里跑。脚本里能用的是我核对过的只读能力：读文件、按名查找、按内容查找、回读已经归档的工具结果，后来又加上记忆召回、历史搜索、任务状态和技能查找。</p><p>写文件、跑命令、起进程、激活技能，以及插件和 MCP 自己报上来的工具，不因为名字像就放进去。能不能进，我认的是宿主登记过的那一份，不认调用方自己填的元数据。排上队、审批回来之后，我再对一次当前登记还在不在。</p><p>脚本里的每一次读取仍走协调器：同一套权限、预算、取消和记录。结果未知的调用我不重放。脚本本身碰不到文件系统、网络和进程。MCP 调用失败时，我把说明文字留在回给模型的内容里。前面如果先塞了一大段资源，不能把失败原因挤掉。凭据在送回之前打掉。</p><h2 id="5-">5. 文件编辑：既要改对位置，也要确认版本</h2><p>文件编辑是我投入比较多的一部分。</p><p>最直接的方案，是让模型输出修改后的完整文件，再整体写回。它容易实现，但为了改几行代码重新生成大型文件，输出成本高，也可能误改无关内容。</p><p>所以我保留了几种局部编辑协议。</p><h3 id="">字符串替换：简单，但要处理歧义</h3><p>模型提供 <code>old_string</code> 和 <code>new_string</code>，工具找到旧文本并替换。它适合明确的小范围修改，对模型也比较直观。</p><p>问题是，同一段代码可能出现多次，复制的缩进、空白或换行也可能不同。</p><p>Biny 优先精确匹配，必要时有限度地容忍格式差异。默认要求匹配位置明确；如果需要替换所有相同片段，则必须显式使用 <code>replace_all</code>。</p><p>出现歧义时，我更倾向于报错，让模型补充上下文，而不是静默选择第一个位置。</p><p><strong>编辑失败可以重新定位，错误地修改成功反而更难发现。</strong></p><h3 id="hashline">Hashline：用行号和摘要核对锚点</h3><p>Hashline 在读取时给每行附上类似 <code>12#A1B2C3D4</code> 的引用标识，编辑时重新核对当前行。</p><p>它解决的是：模型依据旧文件定位，但文件已经变化。如果对应行的位置或内容不再匹配，锚点就会被拒绝。</p><p>这里的示例摘要只是格式演示。它也不是对整份文件语义的证明，更不能代替写入前的版本检查。</p><p>Hashline 目前仍是实验性模式，我没有足够评测数据证明它在整体任务成功率上优于其他方式。</p><h3 id="-patch">结构化 Patch：适配支持对应协议的模型</h3><p>对于支持原生结构化 Patch 的模型，Biny 也提供 <code>apply_patch</code> 入口。模型通过路径、上下文和修改片段表达变更。</p><p>我没有强行把所有模型统一到一种格式，而是依据接口和明确声明的协议适配。几种方式可以放在一起看：</p><table><thead><tr><th> 方式 </th><th> 模型如何描述修改 </th><th> 主要需要核对什么 </th></tr></thead><tbody><tr><td> 字符串替换 </td><td> 旧文本、新文本 </td><td> 匹配是否明确，是否要全部替换 </td></tr><tr><td> Hashline </td><td> 行号与摘要锚点、修改操作 </td><td> 锚点是否仍有效，操作是否冲突 </td></tr><tr><td> Patch </td><td> 文件路径、上下文、增删片段 </td><td> 协议是否合法，上下文能否定位 </td></tr></tbody></table><p>但正确定位，还没有解决写入安全。</p><p>假设 Agent 读取文件后，用户在编辑器里改了同一个文件。Agent 继续写入，就可能覆盖用户的新内容。</p><p>因此，编辑链路还会核对相关文件快照，并检查准备或审批后的目标变化。写入时先生成临时文件，再按创建或覆盖场景提交，同时记录变更证据。</p><p><img src="https://binzi.top/images/biny-tool-system/file-editing.svg" alt="字符串替换、Hashline 和 Patch 汇入文件校验与安全提交链路；版本变化时拒绝继续写入"/></p><p><em>图 3：定位协议回答“改哪里”，文件校验回答“现在还能不能改”。图中是共用约束的归纳，各协议的具体校验步骤并非完全相同。</em></p><p>这不等于绝对的并发安全。版本校验和文件系统提交之间仍有竞态窗口，尤其是外部进程同时原子保存文件时；多文件修改也不能因此视为一个原子事务。</p><p>我目前追求的是减少明显的过期写入和错误覆盖，并在无法确认状态时返回错误。<strong>编辑协议可以变化，底层的一致性约束不能随之丢掉。</strong></p><h2 id="6-">6. 工具结果也需要按需读取</h2><p>工具 Schema 只是上下文的一部分，返回结果同样可能很大。</p><p>搜索整个项目、读取日志、执行一个输出很多内容的 Shell 命令，都可能一次产生大量文本。全部塞进后续请求，会挤占真正用于推理的空间。</p><p>Biny 对超过预算的结果采用归档与受限预览：可归档的完整结果保存在本地，模型先拿到长度受限的内容预览和归档引用，需要更多内容时再通过 <code>read_tool_result</code> 读取。</p><p>这里的预览主要是截取内容，不应理解成辅助模型已经完整读过并做了语义总结。归档本身也有大小和保留数量限制，引用并非永久有效。</p><p>例如命令返回一大段日志，模型可以先看预览定位报错，再回读相关区段。它不必为了查看几条异常，把整份日志反复带进上下文。</p><p>这和按需发现是同一个思路：<strong>工具定义按需披露，工具结果按需读取。</strong></p><h2 id="7-">7. 报错不代表没执行，结果未知也不是普通失败</h2><p>工具恢复最容易出问题的地方，是把“客户端收到错误”理解成“操作没有发生”。</p><p>例如，一个 MCP 工具已经向远端发送创建请求，但收到响应前连接断开。远端可能已经创建成功，只是客户端不知道。</p><p>直接重试，就可能重复创建。</p><p>所以 Biny 会记录执行状态，包括未开始、运行中、副作用已提交、成功、失败、取消和结果未知等。状态记录用于判断执行走到了哪里，而不是只保留一句报错。</p><p><img src="https://binzi.top/images/biny-tool-system/unknown-outcome.svg" alt="远端请求派发后断线，可能已经执行也可能尚未执行；客户端应记录结果未知并核查证据"/></p><p><em>图 4：客户端看见的是同一次断线，远端却可能处于不同状态。因此，断线本身不足以支持安全重试。</em></p><p>对于派发后发生的 MCP 连接错误，当前实现会返回结果未知，而不是自动重放原调用。后续需要结合操作 ID、执行日志和远端已有对象等证据核查。</p><p>操作 ID 能帮助关联记录，但只有远端协议真正支持幂等或去重，它才可能进一步帮助防止重复操作。单有一个本地 ID，并不能保证安全重试。</p><p>同样，记录了文件提交，也不代表后续测试通过；拿到了工具响应，也不一定能证明目标应用中的业务效果符合预期。</p><p><strong>模型说“完成了”，不能替代执行证据。</strong> 恢复任务时，我依据记录下来的执行状态继续。</p><h2 id="">最后</h2><p>做 Biny 的过程中，我逐渐觉得，工具系统的难点不在于接入多少工具，而在于管理好它们从被发现到真正执行的整个过程。</p><p>注册，不等于模型必须看到；看到，不等于获得执行权限；发出请求，也不等于任务已经成功。</p><p>我现在的设计基本围绕这几个边界展开：<strong>让模型按需获得能力，让工具共用一致的执行机制，让文件修改和外部操作有据可查。</strong></p><p>相比继续增加工具数量，我觉得这些底层设计更值得投入。</p><hr/><p><strong>项目地址：</strong> <a href="https://github.com/binyouo/Biny-Agent">Biny Agent</a></p><p><strong>实现索引：</strong></p><ul><li><a href="https://github.com/binyouo/Biny-Agent/blob/52576e4a4dbba3f5753837066ac79b6ac59fb48c/src/tools/registry.ts">工具注册表</a></li><li><a href="https://github.com/binyouo/Biny-Agent/blob/52576e4a4dbba3f5753837066ac79b6ac59fb48c/src/agent/capabilityPreselection.ts">回合前能力预选</a> · <a href="https://github.com/binyouo/Biny-Agent/blob/52576e4a4dbba3f5753837066ac79b6ac59fb48c/src/agent/automaticToolBudget.ts">新增自动工具预算</a></li><li><a href="https://github.com/binyouo/Biny-Agent/blob/52576e4a4dbba3f5753837066ac79b6ac59fb48c/src/tools/toolSearch.ts">ToolSearch 按需发现</a></li><li><a href="https://github.com/binyouo/Biny-Agent/blob/52576e4a4dbba3f5753837066ac79b6ac59fb48c/src/extensions/mcp.ts">MCP 管理</a> · <a href="https://github.com/binyouo/Biny-Agent/blob/52576e4a4dbba3f5753837066ac79b6ac59fb48c/src/runtime/host/resources.ts">Runtime 连接准备</a></li><li><a href="https://github.com/binyouo/Biny-Agent/blob/52576e4a4dbba3f5753837066ac79b6ac59fb48c/src/agent/toolExecutionCoordinator.ts">执行协调器与 Code Mode 子调用</a></li><li><a href="https://github.com/binyouo/Biny-Agent/blob/52576e4a4dbba3f5753837066ac79b6ac59fb48c/src/tools/file/editFile.ts">Edit</a> · <a href="https://github.com/binyouo/Biny-Agent/blob/52576e4a4dbba3f5753837066ac79b6ac59fb48c/src/tools/file/hashline.ts">Hashline</a> · <a href="https://github.com/binyouo/Biny-Agent/blob/52576e4a4dbba3f5753837066ac79b6ac59fb48c/src/tools/file/safeFileIo.ts">安全文件提交</a></li><li><a href="https://github.com/binyouo/Biny-Agent/blob/52576e4a4dbba3f5753837066ac79b6ac59fb48c/src/session/toolResultArchive.ts">工具结果归档</a></li></ul></div></div>]]></description><link>https://binzi.top/posts/ai/biny-agent-tool-system</link><guid isPermaLink="true">https://binzi.top/posts/ai/biny-agent-tool-system</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sun, 11 Oct 2026 18:49:34 GMT</pubDate></item><item><title><![CDATA[Claude Code Projects：一个对话框，管理多个 Agent Session]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/agent/Claude%20Code%20Projects%EF%BC%9A%E4%B8%80%E4%B8%AA%E5%AF%B9%E8%AF%9D%E6%A1%86%EF%BC%8C%E7%AE%A1%E7%90%86%E5%A4%9A%E4%B8%AA%20Agent%20Session">https://binzi.top/posts/agent/Claude%20Code%20Projects%EF%BC%9A%E4%B8%80%E4%B8%AA%E5%AF%B9%E8%AF%9D%E6%A1%86%EF%BC%8C%E7%AE%A1%E7%90%86%E5%A4%9A%E4%B8%AA%20Agent%20Session</a></blockquote><div><p>今天看到了 Claude 的一篇文章，介绍重新设计后的 <strong>Claude Code Projects</strong>。</p><p>我觉得这个功能挺有意思：它在 Claude Code Session 之上增加了一个项目级 Coordinator（协调 Agent），用统一的项目对话来管理多个独立的 Claude Code Session。</p><p>用户只需要在一个对话框里持续提出需求，Coordinator 就可以根据任务情况创建或复用 Thread，完成任务拆分、分发与结果汇总。</p><h2 id="">一、整体架构</h2><p>Claude Code Projects 的结构可以简单理解为：</p><p><strong>Project → Coordinator → Threads（Sessions）</strong></p><ul><li><strong>Project</strong>：项目级工作空间，关联代码仓库或其他上下文，并保存项目指令与共享记忆。</li><li><strong>Coordinator</strong>：负责理解需求、拆分任务、调度 Thread，并跟踪执行进度。</li><li><strong>Thread</strong>：独立的 Claude Code 云端 Session，负责具体任务的执行。</li></ul><p>例如，用户提出一个需求：</p><blockquote><p>分析当前项目的后端架构，优化运行时性能，并补充必要的测试。</p></blockquote>
<p>Coordinator 可以按任务依赖关系拆分工作：</p><ul><li>Thread A：分析架构和性能瓶颈。</li><li>Thread B：根据分析结果优化代码。</li><li>Thread C：补充测试并验证修改。</li></ul><p>每个 Thread 都是完整的 Claude Code Session，而不只是简单的 Subagent。云端 Thread 在独立分支和仓库副本中工作，内部也可以继续使用 Subagent、循环或工作流拆解任务。</p><p>Coordinator 可以创建新的 Thread，也可以把后续任务交给已有 Thread 继续执行。多个 Thread 如果改动了同一处代码，仍然需要像处理普通 PR 一样解决冲突。</p><h2 id="">二、我认为最大的价值是统一交互入口</h2><p>相比多 Agent 并行，我个人更关注它的交互设计。</p><p>对于用户来说，理想的交互方式可能就是<strong>一个 Project 对应一个主要对话框</strong>。</p><p>用户不需要频繁创建新的 Session，也不必为了完成相关任务，在多个对话之间来回切换、重复描述项目背景。</p><p>例如，今天让 Claude 优化 RuntimeHost，明天让它继续修复相关 Bug，后天再补充测试。这些需求都可以直接发送给同一个 Coordinator，由它决定交给哪个 Thread 执行。</p><p>这样用户面对的是一个持续存在的项目，而不是一堆分散的聊天记录。</p><p>底层的 Session 并没有消失。Coordinator 依然会管理多个 Thread，只不过用户不再需要手动处理它们的生命周期和任务分配。</p><p>必要时，用户仍然可以进入具体 Thread，查看代码修改、工具调用以及执行结果。</p><p><strong>统一的是交互入口，而不是所有 Agent 的执行上下文。</strong></p><p>我认为这种设计更符合人的使用习惯。用户应该专注于描述目标和检查结果，而不是花费大量精力组织 Agent 的工作过程。</p><h2 id="">三、那同一个对话框会不会上下文爆炸？</h2><p>这是我觉得比较值得研究的地方。</p><p>虽然用户始终在一个 Project 对话框里交流，但这并不代表所有 Session 的上下文都被塞进同一个模型窗口。</p><p>这里至少可以区分两个层次的上下文。</p><p><strong>首先是 Thread Context。</strong></p><p>每个 Thread 有自己的上下文窗口，独立进行代码阅读、工具调用、修改和测试。Coordinator 会接收 Thread 返回的进展和结果，不需要把每个 Thread 的完整执行历史都当作自己的对话内容。</p><p>因此，多个 Thread 的上下文不会简单累加到 Coordinator 的上下文窗口中。</p><p><strong>其次是 Coordinator Context。</strong></p><p>Coordinator 本身依然有上下文窗口限制。如果用户持续几个月都在同一个 Project 里对话，消息和任务信息也会不断积累。</p><p>Anthropic 公开介绍确认，Project 中的 Thread 会共同写入并读取共享 Memory，帮助后续工作沿用项目细节和决策。官方介绍没有披露 Coordinator 每轮具体加载多少历史消息、采用什么裁剪策略。因此，“结合近期消息、近期 Thread 信息与 Project Memory 来组织上下文”更适合作为一种基于产品描述的理解，而不是已公开确认的实现细节。</p><p>即使有共享 Memory，也不代表彻底解决了上下文问题。较早的需求、设计决策或任务约束，如果没有被正确记录下来，仍然可能在后续执行中丢失。</p><p>因此，<strong>它解决的主要是上下文的隔离和管理，而不是让模型拥有无限上下文。</strong></p><h2 id="">四、我的理解</h2><p>我认为 Claude Code Projects 的核心设计可以概括为：</p><p><strong>Project 负责长期交互与协调，Session 负责具体执行，Memory 负责跨 Session 保留重要信息。</strong></p><p>这几个概念不应该混在一起。</p><p>用户看到的是一个统一且持续存在的项目对话框，而底层实际上由多个具有独立生命周期的 Session 组成。</p><p>从交互体验来看，它减少了用户管理大量 Session 的负担。</p><p>从上下文管理来看，它避免了把所有任务的执行过程集中到同一个模型窗口。</p><p>我觉得这是一个挺合理的方向。</p><p>尤其对于 Coding Agent 来说，<strong>用户真正需要的可能不是管理 Session 的能力，而是让 Agent 自己管理 Session 的能力。</strong></p><p>当然，Coordinator 如何决定创建或复用 Thread、如何维护跨 Session 的任务状态，以及怎样保证长期共享记忆的准确性，仍然是值得继续研究的问题。</p><hr/><p><strong>参考资料</strong></p><ul><li><a href="https://claude.com/blog/projects-redesigned">Claude：Projects redesigned: from folder to conversation</a></li><li><a href="https://code.claude.com/docs/en/claude-projects">Claude Code Projects 官方文档</a></li></ul></div></div>]]></description><link>https://binzi.top/posts/agent/Claude%20Code%20Projects%EF%BC%9A%E4%B8%80%E4%B8%AA%E5%AF%B9%E8%AF%9D%E6%A1%86%EF%BC%8C%E7%AE%A1%E7%90%86%E5%A4%9A%E4%B8%AA%20Agent%20Session</link><guid isPermaLink="true">https://binzi.top/posts/agent/Claude%20Code%20Projects%EF%BC%9A%E4%B8%80%E4%B8%AA%E5%AF%B9%E8%AF%9D%E6%A1%86%EF%BC%8C%E7%AE%A1%E7%90%86%E5%A4%9A%E4%B8%AA%20Agent%20Session</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 10 Oct 2026 04:00:00 GMT</pubDate></item><item><title><![CDATA[用了 Kimi Code 后，我又重新看了一遍 Coding Agent 的 Edit 设计]]></title><description><![CDATA[<link rel="preload" as="image" href="https://binzi.top/images/kimi-code-edit/code-mode-patch.svg"/><link rel="preload" as="image" href="https://binzi.top/images/kimi-code-edit/editing-flow.svg"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/kimi-code-edit-design">https://binzi.top/posts/ai/kimi-code-edit-design</a></blockquote><div><p>最近 GPT-6.1 Sol 实在是有点慢，想着既然要等那么久，不如换 Kimi 试试，刚好搭配 Kimi Code 用了一下。</p><p>用的过程中发现，Kimi Code 的 <code>Edit</code> 设计和 Codex 不太一样。</p><p>Codex 主要采用 <code>apply_patch</code>，让模型通过 Patch 描述文件修改；而 Kimi Code 用的是传统的 <code>old_string → new_string</code> 字符串替换。</p><p>另外还有一个让我感兴趣的设计：<strong>Codex 在 GPT-6 Astra 上就已经使用 Code Mode 编排工具调用，GPT-6.1 Sol 也沿用了这种方式。</strong> 这并不是 Sol 才出现的新特性。</p><p>我个人感觉，这可能和模型能力的提升有关。模型越来越擅长编写代码和处理复杂的工具调用，Harness 也开始让模型承担更多的编排工作。</p><p>当然，Code Mode 和 Patch 并不是互相替代的关系，一个负责工具编排，一个负责文件编辑。</p><p>之前设计 Biny-Agent 的 Edit 时，我其实就思考过这些方案。借着这次使用 Kimi Code，重新翻了一遍源码，也顺便整理一下目前几个 Coding Agent 的设计区别。</p>
<h2 id="1-codex-patch-">1. Codex：以 Patch 为中心</h2><p>Codex 的 <code>apply_patch</code> 是一种结构化文件编辑协议。</p><p>模型不需要输出完整文件，而是提交包含文件路径、上下文及修改内容的 Patch。</p><p>例如：</p><pre class="language-diff lang-diff"><code class="language-diff lang-diff">*** Begin Patch
*** Update File: src/config.ts
@@
-const timeout = 3000;
+const timeout = 5000;
*** End Patch
</code></pre>
<p>它的优势是一次 Patch 可以表达多个修改区域，甚至跨文件的创建、修改、删除和移动。</p><p>从 Codex 的 Rust 源码来看，它会解析 Patch，再通过上下文匹配定位修改位置。</p><p>比较有意思的是，它并非只有严格字符串匹配，还会尝试忽略行尾空白、首尾空白以及部分 Unicode 字符差异。</p><p>这种设计能够减少模型生成 Patch 时因为格式问题导致的失败，但也引入了另一个问题：当文件中存在相似代码时，需要确保上下文能够准确定位目标。</p><p>这里的“上下文匹配”并不等于字符串替换工具的“唯一匹配”。<code>seek_sequence</code> 会从指定位置开始寻找符合条件的序列并返回首次命中，因此重复代码下的定位更依赖充分的上下文。它也不是通过 AST 理解代码语义后再重构。</p><p><strong>Patch 的主要优势是表达能力，而不仅仅是节省 Token。</strong></p><p>但同一次 Patch 能描述多个文件修改，不代表这些修改天然构成一个原子事务。</p><h3 id="codex-code-mode">Codex 的另一个变化：Code Mode</h3><p>本次核对的 Codex 模型配置中，<code>gpt-6-astra</code> 和 <code>gpt-6.1-sol</code> 都设置了 <code>tool_mode: code_mode_only</code>，并保留 <code>apply_patch_tool_type: freeform</code>。这说明 Astra 与 Sol 都采用了这套工具编排方式；仅凭当前配置，还不能确定 Code Mode 最早引入的版本或发布时间。</p><p>这里说的是 <strong>Codex 为模型配置的工具交互方式</strong>，不能直接推导为这些模型在所有产品或 API 场景下都只能使用 Code Mode。</p><p>Code Mode 允许模型通过 JavaScript 组织工具调用，例如先搜索多个文件，再根据结果决定修改哪些内容，而不是所有步骤都由独立的顶层工具调用串联。</p><p>它的价值在于，模型可以用代码组织顺序调用、组合独立请求，并处理工具返回结果。能并行的读取可以一起发起，而依赖读取结果的编辑仍然需要按顺序执行。</p><p>这意味着，Codex 正在把更多工具编排交给模型，但底层的文件修改协议仍然可以使用 Patch。JavaScript 编排本身不提供跨文件回滚，也不会让字符串替换自动变成 Patch。</p><p>我觉得这是值得观察的方向，不过目前还不能简单认为 Code Mode 一定更快，或者一定是模型能力提升后才采用的设计。它仍然需要考虑执行开销、权限和错误处理。</p><p><img src="https://binzi.top/images/kimi-code-edit/code-mode-patch.svg" alt="Code Mode 和 Patch 的关系" height="0" width="0"/></p><p>Code Mode 负责工具编排，Patch 负责描述文件修改，两者可以组合使用。</p><h2 id="2-claude-code">2. Claude Code：精确字符串替换</h2><p>Claude Code 的标准 <code>Edit</code> 采用 <code>old_string</code> 和 <code>new_string</code>。</p><p>模型提供要修改的旧内容和新内容，工具负责匹配并执行替换。</p><p>默认要求旧文本唯一匹配，如果出现多个位置，就需要提供更多上下文，或者使用 <code>replace_all</code>。</p><p>相比 Patch，这种接口明显更简单。</p><p>模型不需要组织 Patch 语法，只需要准确描述原内容和修改后的内容。</p><p>但它也有局限：一旦模型复制的缩进、空格、换行不准确，就可能匹配失败；修改多个不相邻的位置时，也往往需要多次调用 Edit。</p><p>不过 Claude Code 最近在处理文件变化方面做了调整。</p><p>从 v2.1.208 开始，在满足读取权限等条件时，即使文件在上次 Read 后发生了变化，只要 <code>old_string</code> 仍能在当前文件中唯一、精确匹配，也可以继续编辑。</p><p>这点我觉得挺合理。</p><p>文件其他部分发生变化，不代表当前要修改的代码也已经失效。相比单纯通过整个文件版本判断，这能减少部分无意义的失败。</p><p>“编辑前一定要 Read”也需要加上条件。官方文档区分了模型：Opus 4.6、Haiku 4.5 及更早模型仍要求先读；较新模型在 Read 工具可用、读取无需额外权限提示时，可以编辑尚未读取的文件。<code>Read</code> 拒绝规则仍优先生效。这些是 Claude Code 的公开工具行为，不能据此推断其内部锁或磁盘提交实现。</p><h2 id="3-kimi-code">3. Kimi Code：实现很直接</h2><p>这次查看的是 Kimi Code TypeScript 仓库的 <code>419aced0</code> 提交，Edit 实现比较直接。</p><p>核心链路是：</p><p><code>EditTool → FileEditService → EditService</code></p><p>执行时先读取整个文件，再通过字符串匹配得到更新后的内容，最后写回文件。</p><p>所以这里需要区分：</p><p><strong>模型只输出修改的片段，不代表磁盘只写入被修改的几个字节。</strong></p><p>Kimi 的实现本质上仍然是全文件读取、内存替换、全文件写回。</p><p>对于普通代码文件，这种方式我认为没有太大问题。文件读写本身通常远没有模型推理与多轮工具调用昂贵。</p><p>另外，我一开始看到 Kimi Code 显示 <code>+1 -1</code>，还以为它采用了什么不同的修改算法。</p><p>看完 TUI 源码才发现，它是对 <code>old_string</code> 和 <code>new_string</code> 计算 Diff。终端显示出来的很多行可能只是视觉换行，实际文件里仍然是一条逻辑行。</p><p>因此这个统计并不完全等于最终文件的 Git Diff。</p><p>从模型接口来看，我觉得 Kimi Code 的 Edit 确实很优雅：简单、直观，工具定义也不复杂。</p><p>但它在文件版本校验和提交安全方面，仍然有值得进一步核对的地方。在这个提交的 <code>FileEditService</code> 中，可以直接看到 <code>readText → editor.apply → writeText</code>；这一层没有传入上次 Read 的版本标识，也没有显式比较文件快照。这个观察的范围是该服务，不能直接当作所有宿主文件系统和执行层都没有保护的证明。</p><p>旧文本能够匹配，只能说明目标片段当前仍然存在。它不保证周围代码的语义没有变化，也不保证匹配后到写入前没有其他进程修改文件。工具接口简单，不代表底层的并发问题可以忽略。</p><p><img src="https://binzi.top/images/kimi-code-edit/editing-flow.svg" alt="三种 Agent 的文件编辑链路"/></p><p>图中最后一层是概念上的归纳，不表示三种 Agent 使用相同的文件写入实现。</p><h2 id="4-">4. 三种设计怎么选？</h2><table><thead><tr><th> 维度 </th><th> Codex </th><th> Claude Code </th><th> Kimi Code </th></tr></thead><tbody><tr><td> 编辑方式 </td><td> Patch </td><td> String Replace </td><td> String Replace </td></tr><tr><td> 多位置修改 </td><td> 一次 Patch 可描述多个位置 </td><td> 通常多次调用 </td><td> 通常多次调用 </td></tr><tr><td> 匹配策略 </td><td> 上下文匹配，有一定容错 </td><td> 精确匹配 </td><td> 精确匹配 </td></tr><tr><td> 模型输出成本 </td><td> 与 Patch 大小有关 </td><td> 需复制旧文本 </td><td> 需复制旧文本 </td></tr><tr><td> 核心特点 </td><td> 表达力强 </td><td> 接口简单 </td><td> 实现直接 </td></tr></tbody></table>
<p>其实很难判断哪一种方案绝对更好。</p><p>对于小范围修改，字符串替换已经足够。</p><p>对于复杂重构，Patch 可以在一次工具调用中表达更多修改。字符串替换的 <code>replace_all</code> 同样能批量修改多个位置，但这些位置使用的是同一组旧文本和新文本；Patch 可以表达多个内容不同的修改区域，这才是二者更具体的区别。</p><p>但 Patch 也不一定总是更省 Token。如果模型生成的 Patch 频繁失败，需要多次读取和重试，最终成本未必低于字符串替换。</p><p><strong>真正应该衡量的是完成一次正确编辑的总成本，而不是单次 Edit 调用有多简洁。</strong></p><h2 id="5--biny-agent">5. 回到我自己的 Biny-Agent</h2><p>Biny 的 Edit 设计其实在之前就考虑过这些问题。</p><p>目前保留了默认 <code>str_replace</code>、实验性 Hashline，以及针对支持原生结构化 Patch 的模型提供的 <code>apply_patch</code> 适配。</p><p>它们采用不同的修改描述方式，但文件提交尽量复用相同的安全处理逻辑。</p><p>我现在反而不太想继续给 Edit 增加更多功能。</p><p>三套协议并不意味着一定比一套好，增加的匹配规则、执行分支和边界处理，都需要额外维护。</p><p>相比继续讨论哪种协议最先进，我更想通过真实任务对比它们的首次成功率、Token 消耗、重试次数与错误修改率。</p><p>如果简单的字符串替换已经足够稳定，就没有必要为了所谓的架构完整性而引入复杂设计。</p><h2 id="">最后</h2><p>这次从 Kimi Code 的 Edit 看到了一个挺有意思的问题。</p><p>我们经常讨论 Coding Agent 的模型能力，却容易忽略 Harness 里的文件编辑工具。</p><p>同样一个修改需求，可以由模型输出 Patch，也可以使用字符串替换，甚至可以利用行号和内容 Hash 定位。</p><p>不同方案真正的取舍，涉及模型能力、工具调用成本、编辑成功率，以及文件系统的安全边界。</p><p>随着模型能力提升，我觉得 Harness 的一个方向可能是：<strong>让模型承担更多动态编排，让程序继续负责确定性的执行与安全约束。</strong></p><p>但这也只是我目前的观察。Code Mode 是否真的比传统工具调用更有效率，Patch 是否比 String Replace 更适合复杂任务，仍然需要实验验证。</p><p>对 Biny 来说，我现在更倾向于先把现有方案测清楚，而不是继续增加新的编辑协议。</p><hr/><p><strong>参考源码</strong></p><ul><li><a href="https://github.com/openai/codex/tree/4bc83aa5f7ebfec8492bb5747affe0fc8433fcad/codex-rs/apply-patch">Codex — apply_patch</a></li><li><a href="https://github.com/openai/codex/blob/4bc83aa5f7ebfec8492bb5747affe0fc8433fcad/codex-rs/models-manager/models.json">Codex — 模型配置</a></li><li><a href="https://code.claude.com/docs/en/tools-reference">Claude Code — Tools Reference</a></li><li><a href="https://github.com/MoonshotAI/kimi-code/blob/419aced0e97fa04b75f8f71b089e1667f6de6d0a/packages/agent-core-v2/src/agent/tools/edit/editTool.ts">Kimi Code — TypeScript EditTool</a></li><li><p><a href="https://github.com/binyouo/Biny-Agent">Biny-Agent</a></p></li><li><a href="https://github.com/openai/codex/blob/4bc83aa5f7ebfec8492bb5747affe0fc8433fcad/codex-rs/core/src/tools/code_mode/execute_spec.rs">Codex — Code Mode 执行入口</a></li><li><a href="https://github.com/openai/codex/blob/4bc83aa5f7ebfec8492bb5747affe0fc8433fcad/codex-rs/apply-patch/src/seek_sequence.rs">Codex — Patch 上下文匹配</a></li><li><a href="https://github.com/MoonshotAI/kimi-code/blob/419aced0e97fa04b75f8f71b089e1667f6de6d0a/packages/agent-core-v2/src/app/edit/fileEditService.ts">Kimi Code — FileEditService</a></li></ul><p>源码与文档核对于 2026 年 10 月 10 日：Codex <code>4bc83aa5</code>，Kimi Code <code>419aced0</code>；Claude Code 依据当日官方工具参考。使用速度属于个人体验，本文未进行统一性能基准测试。</p></div></div>]]></description><link>https://binzi.top/posts/ai/kimi-code-edit-design</link><guid isPermaLink="true">https://binzi.top/posts/ai/kimi-code-edit-design</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 09 Oct 2026 21:24:48 GMT</pubDate></item><item><title><![CDATA[Pi Agent loop]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/Pi%20Agent%20loop">https://binzi.top/posts/ai/Pi%20Agent%20loop</a></blockquote><div><h2 id="">一、引子：大模型的三种用法</h2><p>在聊 Agent Loop 之前，我们先退一步，看看”使用大模型”这件事本身有几种模式。这对理解”为什么需要循环”至关重要。</p><h3 id="-1--">模式 1：直接调用 —— “模型，回答我”</h3><p>最原始、最直觉的用法。你构建好提示词，调用一次 API，拿到结果，完事。</p><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">用户输入 → 构建提示词 → 调模型 → 模型输出 → 展示结果
</code></pre>
<p>核心工作在于”构建提示词”** 。提示词写得好，结果就好。一次调用，一次输出，没有来回。</p><p>适用场景：翻译、摘要、问答、代码补全——凡是”一问一答”能搞定的事。</p><h3 id="-2workflow--">模式 2：Workflow —— “模型，你先做第一步，我检查一下，再做第二步”</h3><p>当任务变复杂，你发现一次性很难得到满意结果。于是你把大任务拆成多步，每步调一次模型，步骤之间由 <strong>你的代码</strong> 控制流转。</p><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">用户输入 → [步骤1: 调模型分析] → [你的代码: 提取关键信息]
         → [步骤2: 调模型生成草稿] → [你的代码: 检查质量]
         → [步骤3: 调模型润色] → 最终输出
</code></pre>
<p>每一步模型只负责自己的那份工作， <strong>决策权在你手上</strong> ——你知道什么时候该进入下一步，模型只是流水线上的一环。</p><p>适用场景：文档生成流水线、代码审查自动化、RAG（检索增强生成）。</p><h3 id="-3agent-loop--">模式 3：Agent Loop —— “模型，你自己决定怎么做”</h3><p>到了 Agent 模式，你把决策权交给了模型。</p><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">用户输入 → 调模型 → 模型说&quot;我需要读文件&quot; → 执行读文件 → 模型看结果
         → 模型说&quot;还需要搜索代码&quot; → 执行搜索 → 模型看结果
         → 模型说&quot;我知道了，答案是...&quot; → 输出 → 结束
</code></pre>
<p>关键区别： <strong>步骤之间的流转不再由你写死，而是由模型的输出内容来驱动。</strong> 你的代码只做两件事：</p><ol start="1"><li>把用户的输入和工具执行结果喂给模型</li><li>如果模型输出的内容里包含了工具调用请求，就执行它；如果没有，就认为任务完成了</li></ol><p>至于”该调什么工具”、“该调几次”——这些由模型的输出内容决定。“什么时候该停”——这是 <strong>人类定义的规则</strong> ：当模型的一次输出中不再包含工具调用时，我们就认为循环可以结束了。</p><p>用一个对比表格，三种模式的区别一目了然：</p><table><thead><tr><th> 维度 </th><th> 直接调用 </th><th> Workflow </th><th> Agent Loop </th></tr></thead><tbody><tr><td> 决策者 </td><td> 用户 </td><td> 你的代码 </td><td> 模型 </td></tr><tr><td> 模型调用次数 </td><td> 1 次 </td><td> N 次（由代码控制） </td><td> 不确定（由模型控制） </td></tr><tr><td> 核心工作 </td><td> 写提示词 </td><td> 设计流程 </td><td> 定义工具和循环 </td></tr><tr><td> 模型角色 </td><td> 执行者 </td><td> 流水线环节 </td><td> 自主决策者 </td></tr><tr><td> 典型场景 </td><td> 翻译、摘要 </td><td> 文档流水线、RAG </td><td> 编程助手、自动化任务 </td></tr></tbody></table><hr/><h2 id="traceturn">二、Trace、Turn</h2><p>在深入源码之前，有两个概念必须分清楚。它们经常被混用，但在 Pi 的代码里，每个都有精确的含义。</p><h3 id="trace">Trace（一次完整运行）</h3><p>一个 Trace 是从用户按下回车、到 Agent 彻底停下来、发出 <code>agent_end</code> 事件的 <strong>整个过程</strong> 。一个 Trace 包含多个 Turn。</p><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">一个 Trace（一次 agent_start 到 agent_end）
│
├── Turn 1：调模型 → 模型返回 toolUse（要读文件）→ 执行 read 工具
│
├── Turn 2：带着工具结果再调模型 → 模型返回 toolUse（还要改文件）→ 执行 edit 工具
│
└── Turn 3：带着工具结果再调模型 → 模型返回 stop（改好了，没有工具调用）→ agent_end
</code></pre>
<h3 id="turn">Turn（一个轮次）</h3><p>一个 Turn 的定义非常精确： <strong>一次模型调用 + 这次调用触发的所有工具执行。</strong></p><p>每个 Turn 由一对 <code>turn_start</code> 和 <code>turn_end</code> 事件包裹。关键点： <strong>一个 Turn 只有一次模型调用。</strong> 模型返回了 toolUse → 执行那批工具 → 发送 turn_end → 这个 Turn 就结束了。把工具结果喂回去再调模型，那是 <strong>下一个 Turn</strong> 。</p><p>看代码就更清楚了。内层循环每一圈的结构（后面会详讲）：</p><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">while (hasMoreToolCalls || ...) {
    if (!firstTurn) emit(turn_start);    // ← 新 Turn 开始

    处理 pendingMessages
    streamAssistantResponse()             // ← 一次模型调用
    检查 stopReason
    executeToolCalls()                    // ← 执行这个 Turn 触发的一批工具
    emit(turn_end);                       // ← 这个 Turn 结束

    prepareNextTurn / shouldStopAfterTurn / 检查 steering
}
</code></pre>
<p><strong>一圈内层循环 = 一个 Turn = 一次 turn_start → 一次模型调用 → 工具执行 → 一次 turn_end。</strong></p><p>如果模型在一个 Turn 中一口气要求了 3 个工具（read + grep + find），那这 3 个工具都在同一个 Turn 里执行——因为它们都是同一次模型调用的产物。但执行完毕后把结果喂回去再调模型的那一刻，就已经进入下一个 Turn 了。</p><h3 id="-trace--turn-">所以 Trace 和 Turn 的关系就是</h3><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">Trace（一次完整运行）
│  agent_start
│
├── Turn 1
│   │  turn_start
│   ├── 调模型 → toolUse → 执行工具（read + grep）
│   │  turn_end
│   │
├── Turn 2
│   │  turn_start
│   ├── 调模型 → toolUse → 执行工具（edit）
│   │  turn_end
│   │
├── Turn 3
│   │  turn_start
│   ├── 调模型 → stop → 没有工具
│   │  turn_end
│   │
│   agent_end
</code></pre><blockquote>
<p>注意：首轮 Turn 的 <code>turn_start</code> 是在 <code>runAgentLoop()</code> 入口就发出的，然后 <code>runLoop()</code> 内用 <code>firstTurn</code> 标志跳过首圈的 turn_start，避免重复。</p></blockquote>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 560" role="img" aria-label="Trace 与 Turn 的嵌套结构" data-src="https://dg-ai-notes.pages.dev/assets/260702-ch03-trace-turn-nesting.svg"><defs><marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#57534e"></polygon></marker><marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#b5523a"></polygon></marker></defs><rect width="960" height="560" fill="#faf7f2"></rect><rect x="380" y="20" width="200" height="28" rx="4" fill="#faf7f2" stroke="#b5523a" stroke-width="1"></rect><text x="480" y="38" fill="#b5523a" font-size="11" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle" letter-spacing="0.08em">agent_start</text><line x1="480" y1="48" x2="480" y2="76" stroke="#57534e" stroke-width="1" marker-end="url(#arrow)"></line><rect x="40" y="80" width="880" height="380" rx="8" fill="rgba(28,25,23,0.03)" stroke="#1c1917" stroke-width="1.2"></rect><text x="60" y="108" fill="#1c1917" font-size="11" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.14em">TRACE</text><text x="60" y="124" fill="#57534e" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">一次完整运行</text><g><rect x="68" y="140" width="272" height="300" rx="6" fill="#ffffff" stroke="rgba(28,25,23,0.20)" stroke-width="1"></rect><rect x="68" y="140" width="272" height="32" rx="6" fill="rgba(28,25,23,0.05)"></rect><text x="84" y="160" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif">Turn 1</text><rect x="280" y="148" width="52" height="16" rx="2" fill="rgba(181,82,58,0.10)" stroke="rgba(181,82,58,0.50)" stroke-width="0.8"></rect><text x="306" y="160" fill="#b5523a" font-size="8" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle" letter-spacing="0.08em">toolUse</text><text x="84" y="194" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.10em">turn_start</text><rect x="88" y="208" width="232" height="56" rx="4" fill="#ffffff" stroke="#1c1917" stroke-width="1"></rect><text x="204" y="228" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">调用模型</text><text x="204" y="248" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle">streamSimple(model, ctx)</text><line x1="204" y1="264" x2="204" y2="284" stroke="#57534e" stroke-width="1" marker-end="url(#arrow)"></line><rect x="88" y="288" width="232" height="56" rx="4" fill="rgba(181,82,58,0.06)" stroke="#b5523a" stroke-width="1"></rect><text x="204" y="308" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">执行工具（一批）</text><text x="204" y="328" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle">read + grep + find</text><text x="84" y="380" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.10em">turn_end</text><line x1="84" y1="384" x2="324" y2="384" stroke="rgba(28,25,23,0.10)" stroke-width="0.8"></line><text x="84" y="402" fill="#a8a29e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif">stopReason = toolUse</text><text x="84" y="418" fill="#a8a29e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif">→ 把工具结果喂回去</text> </g><g><rect x="344" y="140" width="272" height="300" rx="6" fill="#ffffff" stroke="rgba(28,25,23,0.20)" stroke-width="1"></rect><rect x="344" y="140" width="272" height="32" rx="6" fill="rgba(28,25,23,0.05)"></rect><text x="360" y="160" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif">Turn 2</text><rect x="556" y="148" width="52" height="16" rx="2" fill="rgba(181,82,58,0.10)" stroke="rgba(181,82,58,0.50)" stroke-width="0.8"></rect><text x="582" y="160" fill="#b5523a" font-size="8" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle" letter-spacing="0.08em">toolUse</text><text x="360" y="194" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.10em">turn_start</text><rect x="364" y="208" width="232" height="56" rx="4" fill="#ffffff" stroke="#1c1917" stroke-width="1"></rect><text x="480" y="228" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">再次调用模型</text><text x="480" y="248" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle">消息中带上工具结果</text><line x1="480" y1="264" x2="480" y2="284" stroke="#57534e" stroke-width="1" marker-end="url(#arrow)"></line><rect x="364" y="288" width="232" height="56" rx="4" fill="rgba(181,82,58,0.06)" stroke="#b5523a" stroke-width="1"></rect><text x="480" y="308" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">执行工具（一批）</text><text x="480" y="328" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle">edit</text><text x="360" y="380" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.10em">turn_end</text><line x1="360" y1="384" x2="600" y2="384" stroke="rgba(28,25,23,0.10)" stroke-width="0.8"></line><text x="360" y="402" fill="#a8a29e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif">stopReason = toolUse</text> </g><g><rect x="620" y="140" width="272" height="300" rx="6" fill="#ffffff" stroke="rgba(28,25,23,0.20)" stroke-width="1"></rect><rect x="620" y="140" width="272" height="32" rx="6" fill="rgba(28,25,23,0.05)"></rect><text x="636" y="160" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif">Turn 3</text><rect x="836" y="148" width="44" height="16" rx="2" fill="rgba(28,25,23,0.05)" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"></rect><text x="858" y="160" fill="#57534e" font-size="8" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle" letter-spacing="0.08em">stop</text><text x="636" y="194" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.10em">turn_start</text><rect x="640" y="208" width="232" height="56" rx="4" fill="rgba(181,82,58,0.06)" stroke="#b5523a" stroke-width="1.2"></rect><text x="756" y="228" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">调用模型</text><text x="756" y="248" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle">看到工具结果，给出解释</text><line x1="756" y1="264" x2="756" y2="284" stroke="#57534e" stroke-width="1" stroke-dasharray="4,3"></line><rect x="640" y="288" width="232" height="56" rx="4" fill="rgba(28,25,23,0.02)" stroke="rgba(28,25,23,0.20)" stroke-width="1" stroke-dasharray="4,3"></rect><text x="756" y="308" fill="#a8a29e" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">（无工具调用）</text><text x="756" y="328" fill="#a8a29e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle">toolCalls 为空</text><text x="636" y="380" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.10em">turn_end</text><line x1="636" y1="384" x2="876" y2="384" stroke="rgba(28,25,23,0.10)" stroke-width="0.8"></line><text x="636" y="402" fill="#a8a29e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif">stopReason = stop</text><text x="636" y="418" fill="#a8a29e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif">→ 循环准备退出</text> </g><path d="M 340 290 L 344 290" stroke="#57534e" stroke-width="1" fill="none"></path><path d="M 616 290 L 620 290" stroke="#57534e" stroke-width="1" fill="none"></path><line x1="480" y1="460" x2="480" y2="488" stroke="#57534e" stroke-width="1" marker-end="url(#arrow)"></line><rect x="380" y="492" width="200" height="28" rx="4" fill="#faf7f2" stroke="#b5523a" stroke-width="1"></rect><text x="480" y="510" fill="#b5523a" font-size="11" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle" letter-spacing="0.08em">agent_end</text><line x1="40" y1="534" x2="920" y2="534" stroke="rgba(28,25,23,0.10)" stroke-width="0.8"></line><text x="40" y="550" fill="#57534e" font-size="8" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.14em">LEGEND</text><rect x="100" y="544" width="10" height="8" fill="rgba(181,82,58,0.06)" stroke="#b5523a" stroke-width="0.8"></rect><text x="116" y="551" fill="#57534e" font-size="8" font-family="&#x27;Geist&#x27;, sans-serif">焦点：模型/工具环节</text><rect x="240" y="544" width="10" height="8" fill="rgba(28,25,23,0.05)" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"></rect><text x="256" y="551" fill="#57534e" font-size="8" font-family="&#x27;Geist&#x27;, sans-serif">事件边界</text><text x="340" y="551" fill="#a8a29e" font-size="8" font-family="&#x27;Geist&#x27;, sans-serif" font-style="italic">一个 Turn = 一次模型调用 + 其触发的所有工具执行</text></svg><p>Trace 与 Turn 的嵌套结构</p><p><strong>配图说明</strong> ：一个 Trace 外壳内嵌 3 个 Turn，每个 Turn 都是”模型调用 + 工具执行”的完整闭环。注意 Turn 3 没有 ToolCall（虚线框），它的 stopReason = stop 触发循环退出。</p><hr/><h2 id="">三、全景：一条消息的旅程，以及循环怎么转</h2><p>你输入了”帮我读一下 src/main.ts”并按下回车。从这一刻起发生了什么？让我们站在高处看一遍全过程，同时把”循环怎么转起来的”和”循环什么时候停”一并讲清楚。 <strong>不必纠结每个细节</strong> ——后面会逐段拆解源码。</p><blockquote><p>途中你会遇到四个反复出现的”实体”：你的输入变成 <strong>消息（Message）</strong> ；Loop 调用**模型（Model） <strong>来思考；模型要求的操作用</strong> 工具（Tool） <strong>完成；每一步通过</strong> 事件（Event）**通知外部。驱动这四者反复运转的机制就是本章主角—— <strong>Agent Loop</strong> 。</p></blockquote>
<h3 id="">流程全景</h3><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">你按下回车：&quot;帮我读一下 src/main.ts&quot;
│
│  ① 你的输入变成一条消息
│
UserMessage { role: &quot;user&quot;, content: &quot;帮我读一下 src/main.ts&quot; }
│
│  ② 进入循环（agentLoop 入口）—— agent_start（一个 Trace 开始了）
│
└── runLoop()
    │
    │  ③ 消息转换（AgentMessage → LLM 认识的 Message）
    │
    │  ┌── Turn 1 ──────────────────────────────────────────┐
    │  │  turn_start                                         │
    │  │  ④ 调用 Model（每 Turn 仅一次模型调用）               │
    │  │  streamSimple(model, { systemPrompt, messages })    │
    │  │       ↓ 逐 token 流式返回                            │
    │  │  AssistantMessage {                                  │
    │  │      content: [ ..., ToolCall { name: &quot;read&quot;, ... } ],│
    │  │      stopReason: &quot;toolUse&quot;  ← 有工具调用，继续转      │
    │  │  }                                                  │
    │  │  ⑤ 执行 Tool（工具的五步管道，详见第5章）              │
    │  │  ToolResultMessage { content: [{ text: &quot;文件内容&quot; }] }│
    │  │  turn_end                                            │
    │  └─────────────────────────────────────────────────────┘
    │
    │  循环判断：stopReason 是 toolUse → hasMoreToolCalls = true → 继续
    │
    │  ┌── Turn 2 ──────────────────────────────────────────┐
    │  │  turn_start                                         │
    │  │  ⑥ 第二次调用 Model（工具结果已追加到消息列表）         │
    │  │  streamSimple(model, { messages: [..., toolResult] })│
    │  │       ↓ 模型看到文件内容，开始解释                      │
    │  │  AssistantMessage {                                  │
    │  │      content: [ TextContent { text: &quot;这个文件...&quot; } ],│
    │  │      stopReason: &quot;stop&quot;  ← 没有工具调用，准备停        │
    │  │  }                                                  │
    │  │  turn_end                                            │
    │  └─────────────────────────────────────────────────────┘
    │
    │  循环判断：hasMoreToolCalls = false，pendingMessages 为空
    │  → 内层循环退出
    │  → 外层循环检查 followUp → 空 → 外层循环退出
    │
    └── agent_end（一个 Trace 结束，共 2 个 Turn）
</code></pre>
<h3 id="stopreason--">循环怎么转：stopReason —— 唯一的信号灯</h3><p>整个循环的”油门和刹车”集中在 <strong>一个字段</strong> 上： <code>stopReason</code> 。模型每次返回的 <code>AssistantMessage</code> 里都带着它。</p><p>但在此之前，必须澄清一个关键认知： <strong>模型不会说”我要停了”。</strong> 模型只是个 token 预测器——给定上下文，猜下一个 token，如此反复。它不”知道”任务做完了没有。 <code>stopReason</code> 这个字段虽然挂在模型的返回值上，但它的值来自 <strong>两个不同的地方</strong> ：</p><p><strong>模型 API 真正返回的三种：</strong></p><table><thead><tr><th> stopReason </th><th> 含义 </th></tr></thead><tbody><tr><td> <code>&quot;toolUse&quot;</code> </td><td> 模型输出了工具调用 JSON，API 检测到后返回 </td></tr><tr><td> <code>&quot;stop&quot;</code> </td><td> 生成自然终止（遇到了结束标记），没有工具调用 </td></tr><tr><td> <code>&quot;length&quot;</code> </td><td> token 数达到 maxTokens 上限，被截断 </td></tr></tbody></table><p><strong>框架的流式层注入的两种</strong> （模型 API 本身不会返回这两种值）：</p><table><thead><tr><th> stopReason </th><th> 含义 </th><th> 谁注入的 </th></tr></thead><tbody><tr><td> <code>&quot;error&quot;</code> </td><td> 调用过程异常（网络断了、API 报错等） </td><td> 流式层的 catch 块： <code>output.stopReason = &quot;error&quot;</code> </td></tr><tr><td> <code>&quot;aborted&quot;</code> </td><td> 用户主动中止（AbortSignal 触发） </td><td> 流式层的 catch 块： <code>output.stopReason = &quot;aborted&quot;</code> </td></tr></tbody></table><blockquote><p>代码证据（ <code>packages/ai/src/</code> ）：当 <code>streamSimple</code> 内部的 API 调用抛出异常时，catch 块执行 <code>output.stopReason = options?.signal?.aborted ? &quot;aborted&quot; : &quot;error&quot;</code> 。这不是模型说的，是框架替它”兜底”的。</p></blockquote>
<h3 id="">一条规则驱动整个循环</h3><p>Loop 实际上只看一件事—— <strong>模型输出里有没有工具调用</strong> 。这背后是一条 <strong>人类定义的工程约定</strong> ：</p><blockquote><p>如果模型一次输出中没有工具调用，就认为本轮不需要更多操作，循环可以停了。</p></blockquote>
<p>这不是模型的”智能决策”。换个说法： <strong>不是模型在说”我完成了”，而是我们在说”你没要工具，那就当你完成了”。</strong></p><p>代码里最精炼的判断就是这个（伪代码示意，实际见 <code>agent-loop.ts:202-216</code> ）：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">// 简化逻辑（实际见 agent-loop.ts:202-216）
const toolCalls = message.content.filter(c =&gt; c.type === &quot;toolCall&quot;);
hasMoreToolCalls = false;
if (toolCalls.length &gt; 0) {
  const executedToolBatch = await executeToolCalls(...);
  hasMoreToolCalls = !executedToolBatch.terminate;  // 任何一个工具 terminate 则停止
}
</code></pre><blockquote>
<p><strong>注意</strong> ：实际驱动循环的不是 <code>stopReason === &quot;toolUse&quot;</code> ，而是 <code>toolCalls 数组长度 &gt; 0 &amp;&amp; !terminate</code> 。这意味着：即使 <code>stopReason === &quot;length&quot;</code> （被截断），只要 content 里有 toolCall 块，循环仍会执行工具；反之，即使 <code>stopReason === &quot;toolUse&quot;</code> ，如果所有工具结果都设置 <code>terminate: true</code> ，循环也会停。</p></blockquote>
<p>内层循环的条件是 <code>while (hasMoreToolCalls || pendingMessages.length &gt; 0)</code> ：</p><ul><li>模型返回 toolCall 且工具未 terminate → <code>hasMoreToolCalls = true</code> → <strong>继续转</strong> ：执行工具，把结果喂回去再调模型</li><li><code>stopReason === &quot;stop&quot;</code> 或 <code>&quot;length&quot;</code> 且无 toolCall → <code>hasMoreToolCalls = false</code> → <strong>准备停</strong> （看有没有 pendingMessages）</li><li><code>stopReason === &quot;error&quot;</code> 或 <code>&quot;aborted&quot;</code> → <strong>硬停止</strong> ：立即退出整个循环，不检查 followUp</li></ul><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">┌──────────────────────────────────┐
     │                                  │
     ▼                                  │
┌─────────┐  toolUse   ┌──────────┐    │
│ 调模型   │ ─────────→ │ 执行工具  │    │
└─────────┘            └──────────┘    │
     │                      │          │
     │ stop / length        │ 结果追加  │
     │                      ▼ 到消息    │
     ▼                 重新调模型 ──────┘
┌─────────┐
│ 准备停   │   ← 不是模型决定的，是我们的规则
└─────────┘

error / aborted → 直接跳出整个循环（硬停止）
</code></pre>
<p><strong>为什么不让代码更智能地判断”任务完成没”？</strong> 因为这正是 Agent 和 Workflow 的本质区别。Workflow 里你知道流程有几步，可以用代码判断进度。但 Agent 模式下，你不知道模型要读几个文件、改几处代码——你唯一能稳定依赖的信号就是： <strong>输出里有没有工具调用。</strong> 这既是局限，也是优雅——不需要任何”任务完成度”判断逻辑，代码只做最简单的那层判断。</p><h3 id="">循环的所有退出路径</h3><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 580" role="img" aria-label="stopReason 驱动的循环决策流程" data-src="https://dg-ai-notes.pages.dev/assets/260702-ch03-stopreason-flowchart.svg"><defs><marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#57534e"></polygon></marker><marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#b5523a"></polygon></marker></defs><rect width="960" height="580" fill="#faf7f2"></rect><rect x="320" y="24" width="320" height="64" rx="6" fill="#ffffff" stroke="#1c1917" stroke-width="1"></rect><text x="480" y="48" fill="#1c1917" font-size="12" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">模型返回 AssistantMessage</text><text x="480" y="68" fill="#57534e" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle">message.stopReason</text><line x1="480" y1="88" x2="480" y2="116" stroke="#57534e" stroke-width="1" marker-end="url(#arrow)"></line><polygon points="480,120 600,180 480,240 360,180" fill="rgba(181,82,58,0.06)" stroke="#b5523a" stroke-width="1.2"></polygon><text x="480" y="176" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">检查 stopReason</text><text x="480" y="194" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle">switch</text><line x1="360" y1="180" x2="200" y2="180" stroke="#57534e" stroke-width="1" marker-end="url(#arrow)"></line><rect x="120" y="168" width="76" height="24" rx="2" fill="rgba(181,82,58,0.10)" stroke="rgba(181,82,58,0.50)" stroke-width="0.8"></rect><text x="158" y="184" fill="#b5523a" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle" letter-spacing="0.08em">toolUse</text><line x1="158" y1="192" x2="158" y2="260" stroke="#57534e" stroke-width="1" marker-end="url(#arrow)"></line><rect x="48" y="264" width="220" height="76" rx="6" fill="rgba(181,82,58,0.06)" stroke="#b5523a" stroke-width="1"></rect><text x="158" y="288" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">继续转</text><text x="158" y="306" fill="#57534e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">执行这批工具</text><text x="158" y="322" fill="#57534e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">把结果喂回模型</text><path d="M 48 302 Q 16 302 16 200 Q 16 56 320 56" fill="none" stroke="#b5523a" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#arrow-accent)"></path><rect x="20" y="170" width="60" height="14" rx="2" fill="#faf7f2"></rect><text x="50" y="180" fill="#b5523a" font-size="8" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle" letter-spacing="0.06em">下一 Turn</text><line x1="480" y1="240" x2="480" y2="280" stroke="#57534e" stroke-width="1" marker-end="url(#arrow)"></line><rect x="400" y="268" width="160" height="24" rx="2" fill="rgba(28,25,23,0.05)" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"></rect><text x="480" y="284" fill="#1c1917" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle" letter-spacing="0.08em">stop · length</text><line x1="480" y1="292" x2="480" y2="320" stroke="#57534e" stroke-width="1" marker-end="url(#arrow)"></line><rect x="370" y="324" width="220" height="76" rx="6" fill="#ffffff" stroke="#1c1917" stroke-width="1"></rect><text x="480" y="348" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">准备停</text><text x="480" y="366" fill="#57534e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">检查 followUp / pending 队列</text><text x="480" y="382" fill="#57534e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">两个都空 → 正常退出</text><line x1="480" y1="400" x2="480" y2="448" stroke="#57534e" stroke-width="1" marker-end="url(#arrow)"></line><rect x="380" y="452" width="200" height="32" rx="4" fill="#faf7f2" stroke="#b5523a" stroke-width="1"></rect><text x="480" y="472" fill="#b5523a" font-size="11" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle" letter-spacing="0.08em">agent_end（正常）</text><line x1="600" y1="180" x2="760" y2="180" stroke="#57534e" stroke-width="1" marker-end="url(#arrow)"></line><rect x="768" y="168" width="100" height="24" rx="2" fill="rgba(181,82,58,0.10)" stroke="rgba(181,82,58,0.50)" stroke-width="0.8"></rect><text x="818" y="184" fill="#b5523a" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle" letter-spacing="0.08em">error · aborted</text><line x1="818" y1="192" x2="818" y2="260" stroke="#57534e" stroke-width="1" marker-end="url(#arrow)"></line><rect x="692" y="264" width="252" height="76" rx="6" fill="rgba(28,25,23,0.05)" stroke="#1c1917" stroke-width="1.2"></rect><text x="818" y="288" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">硬停止（fail fast）</text><text x="818" y="306" fill="#57534e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">立即退出整个循环</text><text x="818" y="322" fill="#b5523a" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">不检查 followUp</text><line x1="818" y1="340" x2="818" y2="448" stroke="#57534e" stroke-width="1" marker-end="url(#arrow)"></line><rect x="718" y="452" width="200" height="32" rx="4" fill="#faf7f2" stroke="#b5523a" stroke-width="1.2"></rect><text x="818" y="472" fill="#b5523a" font-size="11" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle" letter-spacing="0.08em">agent_end（硬停）</text><line x1="40" y1="510" x2="920" y2="510" stroke="rgba(28,25,23,0.10)" stroke-width="0.8"></line><text x="40" y="530" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.14em">SOURCE OF stopReason</text><rect x="40" y="540" width="240" height="24" rx="2" fill="#ffffff" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"></rect><text x="56" y="556" fill="#1c1917" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif">来自模型 API</text><text x="180" y="556" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace">stop·length·toolUse</text><rect x="320" y="540" width="280" height="24" rx="2" fill="rgba(181,82,58,0.06)" stroke="rgba(181,82,58,0.50)" stroke-width="0.8"></rect><text x="336" y="556" fill="#1c1917" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif">框架流式层注入</text><text x="448" y="556" fill="#b5523a" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace">error·aborted</text><text x="640" y="556" fill="#a8a29e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" font-style="italic">关键规则：模型不会说&quot;我完成了&quot;——</text><text x="640" y="568" fill="#a8a29e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" font-style="italic">没有 ToolCall 即视为本轮结束</text></svg><p>stopReason 驱动的循环决策流程</p><p><strong>配图说明</strong> ：五种 stopReason 分三路处理——toolUse 让循环继续转；stop/length 准备正常停（仍检查 followUp）；error/aborted 硬停止（不检查 followUp）。注意 stopReason 的两个来源：三种来自模型 API，两种是框架流式层注入的兜底。</p><table><thead><tr><th> 退出路径 </th><th> 触发条件 </th><th> 原因 </th></tr></thead><tbody><tr><td> <strong>正常退出</strong> </td><td> <code>stop</code> / <code>length</code> + 无 followUp + 无 pendingMessages </td><td> 最常见。模型没要工具，也没追加任务 </td></tr><tr><td> <strong>硬停止</strong> </td><td> <code>error</code> / <code>aborted</code> </td><td> 模型调用本身出了问题，继续跑没意义，不检查 followUp </td></tr><tr><td> <strong>外部钩子停</strong> </td><td> <code>shouldStopAfterTurn()</code> 返回 true </td><td> 上下文快满了、达到最大 Turn 数等 </td></tr><tr><td> <strong>工具终止</strong> </td><td> 一批工具的执行结果全部 <code>terminate: true</code> </td><td> 所有工具都同意停止（是 <code>every</code> 不是 <code>some</code> ） </td></tr></tbody></table><hr/><h2 id="-loop--coding-agent-">四、源码详解：基础 Loop 与 coding-agent 的叠加设计</h2><p> 给了你概念全景：消息怎么流动、stopReason 怎么驱动循环、什么时候停。但那都是” <strong>是什么</strong> ”。这一节走进代码，回答” <strong>怎么做到的</strong> ”。</p><p>在看 Pi 的源码之前，先搞清楚一件事： <strong>最简单的 Agent Loop 其实极其简短。</strong></p><h3 id="-loop-agent-">最简 Loop：所有 Agent 的最小公约数</h3><p>剥掉所有产品特性，一个能用的 Agent Loop 只需要这些：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">// 最简 Agent Loop（伪代码）
async function simpleLoop(messages, model, tools) {
    while (true) {
        // ① 调模型
        const response = await callModel(model, messages, tools);
        messages.push(response);

        // ② 没有工具调用 → 结束
        if (response.stopReason !== &quot;toolUse&quot;) {
            return messages;
        }

        // ③ 有工具调用 → 执行，把结果喂回去
        for (const toolCall of response.toolCalls) {
            const result = await executeTool(toolCall);
            messages.push(result);
        }
    }
}
</code></pre>
<p>十几行代码。一个 while 循环，调模型、执行工具、再调模型，直到模型不再要求工具。这就是 §三 讲的那套逻辑的最小实现—— <strong>任何 Agent 都需要这个内核</strong> 。</p><h3 id="pi--coding-agent-">Pi 的 coding-agent 在此基础上叠加了什么</h3><p>Pi 的 coding-agent 是一个 <strong>交互式编程助手</strong> ——用户在终端里跟它对话，它可能要读好几个文件、改代码、跑测试。这种产品场景比”最简 Loop”多出了真实的需求：</p><table><thead><tr><th> 真实需求 </th><th> 叠加的设计 </th><th> 源码位置 </th></tr></thead><tbody><tr><td> 用户在 Agent 工作期间又输入了新指令 </td><td> <strong>steering 消息注入</strong> ：紧急消息可以在 Turn 之间插队 </td><td> 内层循环开头 </td></tr><tr><td> 系统在 Agent 完成后想追加后续任务（如”顺便跑个测试”） </td><td> <strong>外层 followUp 循环</strong> ：内层停了但外层可以重启内层 </td><td> 外层 while(true) </td></tr><tr><td> 不同复杂度的任务想用不同档次的模型 </td><td> <strong>prepareNextTurn 钩子</strong> ：每个 Turn 结束时可切换模型/上下文 </td><td> turn_end 之后 </td></tr><tr><td> 上下文窗口快满了需要触发压缩 </td><td> <strong>shouldStopAfterTurn 钩子</strong> ：外部判断是否该停 </td><td> prepareNextTurn 之后 </td></tr></tbody></table><p><strong>关键认知</strong> ：这些叠加设计都是 coding-agent 的 <strong>功能选择</strong> ，不是 Agent 的通用法则。如果你做的是一个”一问一答带工具”的简单 Agent，上面这张表全是多余的——你只需要最简 Loop。</p><p>但理解 coding-agent 怎么叠加这些设计很有价值——你自己的产品场景很可能也需要类似机制。接下来，我们以 coding-agent 的完整源码为例，逐段走这些设计。跟着那条”帮我读一下 src/main.ts”的消息，走完从入口到结束的整段旅程。</p><h3 id="41-runagentloop-">4.1 入口：runAgentLoop() 收到了什么</h3><blockquote><p>§三简要展示了流程全景，这里展开看代码细节——同一个过程，更深入的视角。</p></blockquote>
<p>你按下回车后，调用链是： <code>Agent.prompt()</code> → <code>runPromptMessages()</code> → <code>runAgentLoop()</code> 。停在入口：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">// agent-loop.ts:95-118
async function runAgentLoop(
    prompts: AgentMessage[],     // 你的消息
    context: AgentContext,       // 当前对话上下文（快照副本）
    config: AgentLoopConfig,     // 循环配置（模型、钩子、队列回调）
    emit: AgentEventSink,        // 事件发射器
    signal?: AbortSignal,        // 中止信号
    streamFn?: StreamFn,         // 流式函数（可替换）
): Promise&lt;AgentMessage[]&gt;
</code></pre>
<p>六个参数中最重要的三个：</p><p><strong><code>prompts</code></strong> — 你的消息已经被包装成了标准格式：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">[{
  role: &quot;user&quot;,
  content: [{ type: &quot;text&quot;, text: &quot;帮我读一下 src/main.ts&quot; }],
  timestamp: 1748000000000
}]
</code></pre>
<p><strong><code>context</code></strong> — 对话上下文快照。注意是 <strong>副本</strong> （ <code>agent.ts:414-420</code> 的 <code>createContextSnapshot()</code> 创建），Loop 运行期间对 context 的修改不会影响 Agent 类的原始状态：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">{
  systemPrompt: &quot;You are a helpful coding assistant...&quot;,
  messages: [ /* 之前的对话历史 */ ],
  tools: [
    { name: &quot;read&quot;, description: &quot;...&quot;, parameters: Type.Object({...}), execute: ... },
    { name: &quot;bash&quot;, description: &quot;...&quot;, parameters: Type.Object({...}), execute: ... },
  ]
}
</code></pre>
<p><strong><code>config</code></strong> — Loop 的行为配置。这里有一组关键钩子（都是函数，不是数据）：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">{
  model: Model,                    // 用哪个 LLM
  convertToLlm: Function,          // AgentMessage[] → Message[] 转换
  transformContext?: Function,      // 调 LLM 前的上下文预处理（如压缩）
  getSteeringMessages?: Function,   // 获取&quot;紧急插队&quot;消息
  getFollowUpMessages?: Function,   // 获取&quot;追加任务&quot;消息
  shouldStopAfterTurn?: Function,   // 每轮结束后是否该停
  beforeToolCall?: Function,        // 工具执行前钩子
  afterToolCall?: Function,         // 工具执行后钩子
  toolExecution: &quot;parallel&quot;,        // 工具执行模式
}
</code></pre>
<p>这些钩子都是 <strong>函数</strong> 而非数据——Loop 在运行时调用它们来”拉取”最新状态。这让 Loop 和外部消息来源彻底解耦。</p><p>入口函数只做了三步准备：</p><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">Step 1: 创建 newMessages 数组
        → 收集本轮 Trace 产生的所有新消息

Step 2: 把 prompts 追加到 context.messages
        → context.messages = [...context.messages, ...prompts]

Step 3: 发初始事件
        → emit(&quot;agent_start&quot;)    ← Trace 开始
        → emit(&quot;turn_start&quot;)     ← 首轮 Turn 开始（入口就发，后续 Turn 在内层循环里发）
        → 对每条 prompt：emit(&quot;message_start&quot;) + emit(&quot;message_end&quot;)
        → 调用 runLoop()
</code></pre>
<p>数据变化：</p><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">入口前：
  context.messages = [user1, asst1, toolResult1]    ← 之前的对话
  newMessages = []

入口后：
  context.messages = [user1, asst1, toolResult1, user2]  ← 你的消息被追加
  newMessages = [user2]                                   ← 收集器开始记录
</code></pre>
<hr/><h3 id="42-runloop-">4.2 runLoop() 的骨架：先看内核，再看叠加</h3><p>现在进入 <code>runLoop()</code> ——整个系统最核心的代码。别被它的长度吓到，我们先看 <strong>内核</strong> ，再看 <strong>叠加</strong> 。</p><h4 id="">内核：内层循环</h4><p>如果只保留最简 Loop 的逻辑， <code>runLoop</code> 长这样：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">// 只保留内核的 runLoop（伪代码）
while (hasMoreToolCalls) {
    // 步骤 B：调 LLM
    // 步骤 C：检查 stopReason → error/aborted 就退出
    // 步骤 D：执行工具
    // 步骤 E：emit turn_end
}
// 结束 → emit agent_end
</code></pre>
<p>这就是最简 Loop——调模型、执行工具、turn_end，循环往复。内层循环的退出条件 <code>hasMoreToolCalls</code> 由 <code>toolCalls 数组长度 &gt; 0 &amp;&amp; !terminate</code> 驱动（§三讲过）。 <strong>这段是所有 Agent 都需要的内核。</strong></p><h4 id="coding-agent-">叠加：coding-agent 加了两层外壳</h4><p>但 coding-agent 作为交互式编程助手，需要在内核外面加两样东西：</p><p><strong>叠加 1：steering 消息注入</strong> （内层循环开头 + 每圈结尾各检查一次）。用户在 Agent 工作时输入了新指令——这些消息不能等当前任务跑完，得在下一圈开头紧急注入。所以内层循环条件多了一个 <code>|| pendingMessages.length &gt; 0</code> 。</p><p><strong>叠加 2：外层 followUp 循环</strong> （包在整个内层循环外面）。Agent 自然停了之后，系统可能还想追加任务（比如”顺便跑个测试”）。外层循环让这些追加任务在 <strong>同一个 Trace 内</strong> 继续跑，不需要重新启动一个新的 Loop。</p><p>把内核和两层叠加拼起来，才是完整的 <code>runLoop</code> 骨架：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">async function runLoop(currentContext, newMessages, config, signal, emit, streamFn) {

    // ① 首次 steering 检查（在进入内层循环之前！）
    let pendingMessages = (await config.getSteeringMessages?.()) || [];

    // ========== 叠加2：外层循环（followUp 续命）==========
    while (true) {
        let hasMoreToolCalls = true;
        let firstTurn = true;  // 首轮跳过 turn_start（入口已发）

        // ========== 内核 + 叠加1：内层循环 ==========
        while (hasMoreToolCalls || pendingMessages.length &gt; 0) {
            //                                    ↑ 叠加1：steering 消息也驱动循环

            if (!firstTurn) {
                emit({ type: &quot;turn_start&quot; });
            }
            firstTurn = false;

            // 步骤 A：注入 pendingMessages（steering 消息）← 叠加1
            // 步骤 B：调 LLM → streamAssistantResponse()  ← 内核
            // 步骤 C：检查 stopReason                      ← 内核
            // 步骤 D：执行工具                              ← 内核
            // 步骤 E：emit turn_end                         ← 内核
            // 步骤 F：prepareNextTurn → shouldStopAfterTurn ← 叠加（钩子）
            //         → 再次检查 steering                    ← 叠加1
        }

        // ========== 内层循环结束 ==========
        // 叠加2：检查 followUp 队列
        const followUpMessages = (await config.getFollowUpMessages?.()) || [];
        if (followUpMessages.length &gt; 0) {
            pendingMessages = followUpMessages;
            continue;  // 回到外层循环顶部，内层循环重开
        }

        break;  // 两个队列都空，真正退出
    }
}
</code></pre>
<p>现在我们逐步骤展开。每个步骤会标注”内核”还是”叠加”，方便你区分。</p><hr/><h3 id="43-1--asteering-">4.3 【叠加1 · 步骤A】steering 消息注入</h3><blockquote><p><strong>什么是 steering？</strong> 这是 coding-agent 的一个交互功能。想象你让 Agent 帮你修一个 bug，Agent 正在读文件、分析代码。这时候你突然想到一个补充：“也检查一下测试文件”——你希望这条指令能 <strong>插队</strong> ，而不是等 Agent 把当前任务做完再说。</p></blockquote>
<p>steering 就是这个”插队”机制。用户在 Agent 工作期间输入的新指令，会被放进 steering 队列。每圈内层循环开头，Loop 先检查这个队列，把紧急消息注入到当前对话中：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">if (pendingMessages.length &gt; 0) {
    for (const message of pendingMessages) {
        await emit({ type: &quot;message_start&quot;, message });
        await emit({ type: &quot;message_end&quot;, message });
        currentContext.messages.push(message);
        newMessages.push(message);
    }
    pendingMessages = [];  // 消费完毕，清空
}
</code></pre>
<p>这段代码就是把紧急消息逐条注入到上下文和消息收集器中。</p><p>pendingMessages 的第一个来源是 <code>runLoop</code> 一进来就执行的首次 steering 检查（ <code>agent-loop.ts:167</code> ）。为什么要在进入循环 <strong>之前</strong> 就检查？因为用户在等待 LLM 首次响应时可能又输入了内容——这时候消息已经从外部排队了，但循环还没开始，如果不提前取出来，这批消息就漏掉了。</p><hr/><h3 id="44---bstreamassistantresponse---llm">4.4 【内核 · 步骤B】streamAssistantResponse() — 调 LLM</h3><p>这是整个 Loop 最重的一步——把消息发给模型、拿回流式响应。分为四个阶段。</p><h4 id="-a">阶段 A：上下文预处理（可选）</h4><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">let messages = context.messages;
if (config.transformContext) {
    messages = await config.transformContext(messages, signal);
}
</code></pre>
<p>如果配置了 <code>transformContext</code> （如压缩算法），在此预处理消息。不配置就跳过。</p><h4 id="-bagentmessage--message-">阶段 B：AgentMessage → Message 转换（两层消息的边界）</h4><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">const llmMessages = await config.convertToLlm(messages);
</code></pre>
<p>这一行站在 Agent 内核和 LLM 的 <strong>边界</strong> 上。要理解它为什么存在，得先知道”两层消息”的设计。</p><p>Agent 内部维护对话历史时，需要记录的不只是”用户说了什么、AI 回了什么”——它还需要记录 <strong>自己的内部状态</strong> 。比如 coding-agent 会记录：上下文被压缩过（ <code>CompactionSummaryMessage</code> ）、Bash 命令的执行详情（ <code>BashExecutionMessage</code> ）、分支切换的记录（ <code>BranchSummaryMessage</code> ）。这些是 Agent 自己用的”内部语言”， <strong>LLM 根本不认识这些消息类型</strong> ——它只认三种标准消息： <code>UserMessage</code> 、 <code>AssistantMessage</code> 、 <code>ToolResultMessage</code> 。</p><p><code>convertToLlm</code> 就是站在这个边界上的 <strong>翻译官</strong> ：把 Agent 的内部语言翻译成 LLM 能理解的协议。默认实现就是一个 <code>.filter()</code> ——只保留三种标准消息：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">function defaultConvertToLlm(messages: AgentMessage[]): Message[] {
    return messages.filter(
        (message) =&gt; message.role === &quot;user&quot;
                  || message.role === &quot;assistant&quot;
                  || message.role === &quot;toolResult&quot;,
    );
}
</code></pre>
<p>数据变换：</p><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">转换前（AgentMessage[]）：
[
  { role: &quot;user&quot;, content: &quot;帮我读一下 src/main.ts&quot;, ... },    ← 保留
  { role: &quot;assistant&quot;, content: [...], ... },                   ← 保留
  { role: &quot;compactionSummary&quot;, summary: &quot;之前的对话摘要...&quot; },   ← 过滤掉
  { role: &quot;toolResult&quot;, content: [...], ... },                  ← 保留
]

转换后（Message[]）：
[
  { role: &quot;user&quot;, content: &quot;帮我读一下 src/main.ts&quot;, ... },
  { role: &quot;assistant&quot;, content: [...], ... },
  { role: &quot;toolResult&quot;, content: [...], ... },
]
</code></pre><blockquote>
<p>两层消息的完整设计，详见《第6章：消息系统》。</p></blockquote>
<h4 id="-c-context-">阶段 C：构建 Context 并调用模型</h4><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">const llmContext: Context = {
    systemPrompt: context.systemPrompt,
    messages: llmMessages,
    tools: context.tools,
};

const streamFunction = streamFn || streamSimple;
const resolvedApiKey =
    (config.getApiKey ? await config.getApiKey(config.model.provider) : undefined)
    || config.apiKey;

const response = await streamFunction(config.model, llmContext, {
    ...config,
    apiKey: resolvedApiKey,
    signal,
});
</code></pre>
<p><strong>构建 Context 是这一步的主线。</strong> 注意 <code>llmContext</code> 是一个 <strong>全新对象</strong> ，每圈内层循环都重建一次。它由三部分组成：</p><ul><li><strong><code>systemPrompt</code></strong> —— 直接复用 Agent 上下文里的系统提示词，告诉模型”你是谁、该遵守什么规则”</li><li><strong><code>messages</code></strong> —— 就是上一步 <code>convertToLlm</code> 过滤后的 <code>llmMessages</code> ，只含 LLM 认识的三种标准消息</li><li><strong><code>tools</code></strong> —— 工具列表（带 schema 定义），让模型知道”这次有哪些工具可以调”</li></ul><p>注意一个细节： <code>llmContext.tools = context.tools</code> 是 <strong>引用赋值</strong> ——每圈虽然包了个新的 wrapper 对象，但 <code>tools</code> 数组本身是同一份引用，内容字节级稳定。 <code>systemPrompt</code> 同理。只有 <code>messages</code> 真的在长（每圈追加新的 ToolResultMessage）。</p><p>那为什么还要每圈重建 <code>llmContext</code> 这个 wrapper？因为有的 Turn 确实会改这三者里的某一个： <code>prepareNextTurn</code> 钩子（§4.7）可能换过模型或改过 systemPrompt，扩展系统（§5）可能动态注册新工具。重建 wrapper 的成本可以忽略（一个 JS 对象），但能保证不会因为共享引用导致难以追踪的状态污染。</p><p><strong>这会不会破坏 prompt cache？</strong> 不会。Anthropic 的 prompt cache 是 <strong>内容寻址</strong> 的——它看的是发过去的字节，不是请求的 identity。每次发的是新对象还是旧对象无所谓，只要 <code>system + tools</code> 的字节不变，cache 就命中。Pi 在 <a href="https://github.com/earendil-works/pi/blob/main/packages/ai/src/api/anthropic-messages.ts">anthropic-messages.ts</a> 里显式在 <strong>三个位置</strong> 打 <code>cache_control: { type: &quot;ephemeral&quot; }</code> 标记：</p><table><thead><tr><th> 位置 </th><th> 源码行 </th><th> 作用 </th></tr></thead><tbody><tr><td> System prompt 末尾 </td><td> <a href="https://github.com/earendil-works/pi/blob/main/packages/ai/src/api/anthropic-messages.ts#L922">L922/929/938</a> </td><td> 系统提示词整体作为可缓存前缀 </td></tr><tr><td> <strong>最后一个 tool</strong> </td><td> <a href="https://github.com/earendil-works/pi/blob/main/packages/ai/src/api/anthropic-messages.ts#L1208">L1208</a> </td><td> 整个 tools 列表作为可缓存前缀 </td></tr><tr><td> <strong>最后一条 user message</strong> </td><td> <a href="https://github.com/earendil-works/pi/blob/main/packages/ai/src/api/anthropic-messages.ts#L1157">L1157-1178</a> </td><td> <strong>rolling cache</strong> ——每 Turn 把 cache 推进到最新消息 </td></tr></tbody></table><p>第三条尤其精妙：cache breakpoint 不是固定在第一条消息上，而是 <strong>跟着最新 user message 走</strong> 。这样旧前缀继续命中、新追加的内容被写入，整个对话历史都享受 cache 收益。命中链路大致是：</p><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">Turn 1: 写入 [system + tools] → 写入 [messages §1]
Turn 2: 命中 [system + tools] → 命中 [messages §1] → 写入 [messages §2]
Turn 3: 命中 [system + tools] → 命中 [messages §1+§2] → 写入 [messages §3]
</code></pre>
<p>还有个容易误解的点： <strong>tools 不是被塞到 messages 末尾</strong> 。Anthropic API 协议里 <code>tools</code> 是独立的顶层字段（位置在 messages 之前），这个协议设计本身就考虑了 cache——稳定的 tools 在前、变动的 messages 在后，prefix 越长越省。</p><p>OpenAI 体系走的是另一条路（ <a href="https://github.com/earendil-works/pi/blob/main/packages/ai/src/api/openai-completions.ts#L554">openai-completions.ts:554</a> ）： <code>prompt_cache_key: sessionId</code> ，OpenAI 后端按 session 自动匹配前缀。DeepSeek、Qwen 等通过 <code>cacheControlFormat: &quot;anthropic&quot;</code> 兼容字段，也能复用 Anthropic 风格的 cache_control 标记（ <a href="https://github.com/earendil-works/pi/blob/main/packages/ai/src/api/openai-completions.ts#L593">L593</a> 的 <code>applyAnthropicCacheControl</code> ）。</p><h4 id="-d--">阶段 D：流式处理响应 —— 原地替换的妙用</h4><p><code>streamFunction</code> 返回的是 <code>AssistantMessageEventStream</code> ——一个异步迭代器。这步有一个很精巧的设计：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">for await (const event of response) {
    switch (event.type) {
        case &quot;start&quot;:
            // 拿到一个&quot;空壳&quot;消息，直接 push 到 context
            partialMessage = event.partial;
            context.messages.push(partialMessage);
            emit({ type: &quot;message_start&quot;, ... });
            break;

        case &quot;text_delta&quot;:       // 文本增量
        case &quot;toolcall_delta&quot;:   // 工具调用增量
        case &quot;thinking_delta&quot;:   // 思考增量
            partialMessage = event.partial;            // 更新后的部分消息
            context.messages[last] = partialMessage;    // ★ 原地替换！
            emit({ type: &quot;message_update&quot;, ... });      // UI 收到增量更新
            break;

        case &quot;done&quot;:
        case &quot;error&quot;:
            finalMessage = await response.result();
            context.messages[last] = finalMessage;       // ★ 用最终完整消息替换
            emit({ type: &quot;message_end&quot;, ... });
            return finalMessage;
    }
}
</code></pre>
<p>为什么要先 push 空壳再原地替换？注意”原地替换”的意思——不是往 <code>context.messages</code> 数组里 push 新条目，而是 <strong>用新内容覆盖最后一条</strong> （ <code>context.messages[last] = partialMessage</code> ）。这样 context 的消息数量不变，但最后一条消息的内容在”长大”。</p><p>为了 <strong>UI 能实时展示</strong> 。如果等全部响应完成才 push 到 context，用户在 LLM 思考的几秒里盯着空白屏幕。通过”先放空壳、逐 token 替换”，UI 通过 <code>message_update</code> 事件拿到最新部分消息，能做到逐字渲染。</p><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" role="img" aria-label="流式响应原地替换的时间线" data-src="https://dg-ai-notes.pages.dev/assets/260702-ch03-streaming-replace.svg"><defs><marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#57534e"></polygon></marker></defs><rect width="960" height="540" fill="#faf7f2"></rect><text x="40" y="60" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.14em">EVENT</text><text x="320" y="60" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.14em">context.messages[last] 内容</text><text x="720" y="60" fill="#57534e" font-size="9" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.14em">UI 显示</text><line x1="40" y1="76" x2="920" y2="76" stroke="rgba(28,25,23,0.15)" stroke-width="0.8"></line><text x="40" y="120" fill="#b5523a" font-size="11" font-family="&#x27;Geist Mono&#x27;, monospace">start</text><line x1="100" y1="116" x2="120" y2="116" stroke="#b5523a" stroke-width="1" marker-end="url(#arrow)"></line><text x="135" y="120" fill="#57534e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif">push 空壳</text><rect x="320" y="100" width="360" height="44" rx="4" fill="rgba(28,25,23,0.03)" stroke="rgba(28,25,23,0.30)" stroke-width="1" stroke-dasharray="3,2"></rect><text x="340" y="118" fill="#57534e" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace">{ role: &quot;assistant&quot;,</text><text x="340" y="134" fill="#a8a29e" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace">content: [] ← 空壳 }</text><rect x="720" y="100" width="180" height="44" rx="4" fill="rgba(28,25,23,0.03)" stroke="rgba(28,25,23,0.30)" stroke-width="1"></rect><text x="810" y="126" fill="#a8a29e" font-size="11" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">（空白）</text><line x1="40" y1="158" x2="920" y2="158" stroke="rgba(28,25,23,0.08)" stroke-width="0.6"></line><text x="40" y="194" fill="#b5523a" font-size="11" font-family="&#x27;Geist Mono&#x27;, monospace">text_delta</text><line x1="120" y1="190" x2="140" y2="190" stroke="#b5523a" stroke-width="1" marker-end="url(#arrow)"></line><text x="155" y="194" fill="#57534e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif">原地替换</text><rect x="320" y="174" width="360" height="44" rx="4" fill="rgba(181,82,58,0.06)" stroke="#b5523a" stroke-width="1.2"></rect><text x="340" y="192" fill="#1c1917" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace">{ content: [</text><text x="340" y="208" fill="#b5523a" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace">{ type:&quot;text&quot;, text:&quot;好的...&quot; }</text><text x="490" y="208" fill="#57534e" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace">] }</text><rect x="720" y="174" width="180" height="44" rx="4" fill="#ffffff" stroke="rgba(28,25,23,0.30)" stroke-width="1"></rect><text x="810" y="200" fill="#1c1917" font-size="11" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">好的...</text><line x1="40" y1="232" x2="920" y2="232" stroke="rgba(28,25,23,0.08)" stroke-width="0.6"></line><text x="40" y="268" fill="#b5523a" font-size="11" font-family="&#x27;Geist Mono&#x27;, monospace">toolcall_delta</text><line x1="130" y1="264" x2="150" y2="264" stroke="#b5523a" stroke-width="1" marker-end="url(#arrow)"></line><text x="165" y="268" fill="#57534e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif">再次原地替换</text><rect x="320" y="248" width="360" height="44" rx="4" fill="rgba(181,82,58,0.06)" stroke="#b5523a" stroke-width="1.2"></rect><text x="340" y="266" fill="#1c1917" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace">{ content: [ text:&quot;好的...&quot;,</text><text x="340" y="282" fill="#b5523a" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace">{ type:&quot;toolCall&quot;, name:&quot;read&quot; } ] }</text><rect x="720" y="248" width="180" height="44" rx="4" fill="#ffffff" stroke="rgba(28,25,23,0.30)" stroke-width="1"></rect><text x="810" y="270" fill="#1c1917" font-size="11" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">好的...</text><text x="810" y="284" fill="#b5523a" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">+<p> 工具调用</p></text><line x1="40" y1="306" x2="920" y2="306" stroke="rgba(28,25,23,0.08)" stroke-width="0.6"></line><text x="40" y="342" fill="#b5523a" font-size="11" font-family="&#x27;Geist Mono&#x27;, monospace">done</text><line x1="80" y1="338" x2="100" y2="338" stroke="#b5523a" stroke-width="1" marker-end="url(#arrow)"></line><text x="115" y="342" fill="#57534e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif">最终完整消息替换</text><rect x="320" y="322" width="360" height="44" rx="4" fill="rgba(181,82,58,0.10)" stroke="#b5523a" stroke-width="1.4"></rect><text x="340" y="340" fill="#1c1917" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace">{ content:[...],</text><text x="340" y="356" fill="#b5523a" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace">stopReason:&quot;toolUse&quot;, usage:{...} }</text><rect x="720" y="322" width="180" height="44" rx="4" fill="#ffffff" stroke="rgba(28,25,23,0.30)" stroke-width="1"></rect><text x="810" y="348" fill="#1c1917" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">完整渲染 ✓</text><rect x="40" y="412" width="880" height="100" rx="6" fill="rgba(181,82,58,0.05)" stroke="rgba(181,82,58,0.30)" stroke-width="1" stroke-dasharray="4,4"></rect><text x="60" y="438" fill="#b5523a" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.14em">WHY</text><text x="60" y="462" fill="#1c1917" font-size="11" font-family="&#x27;Instrument Serif&#x27;, &#x27;Source Han Serif SC&#x27;, serif" font-style="italic">为什么先 push 空壳再原地替换？ </text><text x="60" y="482" fill="#57534e" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">· 等全部响应完成才 push → 用户在 LLM 思考的几秒里盯着空白屏幕 </text><text x="60" y="498" fill="#57534e" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">· 先放空壳、逐 token 替换 → UI 通过 message_update 事件拿到最新部分消息，能做到逐字渲染</text></svg><p>流式响应原地替换的时间线</p><p><strong>配图说明</strong> ：四个时间点的 context.messages[last] 演变——start 时空壳、text_delta 时文字在长、toolcall_delta 时工具调用出现、done 时最终完整消息替换。每一步同步展示 UI 显示状态。</p><p>数据在流式响应期间， <code>context.messages[last]</code> 的演变：</p><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">start    → { role: &quot;assistant&quot;, content: [] }                    ← 空壳 push
text_delta → { content: [{ type:&quot;text&quot;, text:&quot;好的...&quot; }] }       ← 文字在长
toolcall   → { content: [{ text:&quot;好的...&quot; },                        ← 工具调用出现
                         { type:&quot;toolCall&quot;, name:&quot;read&quot;, arguments:{file_path:&quot;src/main.ts&quot;} }] }
done     → { content: [...], stopReason:&quot;toolUse&quot;, usage:{...} } ← 最终完整消息替换
</code></pre>
<hr/><h3 id="45---c-stopreason">4.5 【内核 · 步骤C】检查 stopReason</h3><p>§三详细讲了 <code>stopReason</code> 的来源和含义。这里看代码怎么处理它。拿到 <code>AssistantMessage</code> 后，立即做一次硬停止判断：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">// agent-loop.ts:196-200
if (message.stopReason === &quot;error&quot; || message.stopReason === &quot;aborted&quot;) {
    await emit({ type: &quot;turn_end&quot;, message, toolResults: [] });
    await emit({ type: &quot;agent_end&quot;, messages: newMessages });
    return;   // ← 直接退出整个 runLoop，不检查 followUp
}
</code></pre>
<p><code>error</code> 和 <code>aborted</code> 是”硬停止”——立即发 turn_end + agent_end，直接 return。连工具都不执行，连 followUp 都不检查。这是一种 <strong>快速失败</strong> （fail fast）策略：既然模型调用本身就失败了（网络异常或用户中止），继续跑没有任何意义。</p><p>而 <code>stop</code> 、 <code>toolUse</code> 、 <code>length</code> 这三种，代码继续往下走——进入工具执行逻辑。区别在于： <code>stop</code> 和 <code>length</code> 时， <code>toolCalls</code> 数组为空，所以工具执行步骤什么也不干。</p><hr/><h3 id="46---dexecutetoolcalls--">4.6 【内核 · 步骤D】executeToolCalls() — 执行工具</h3><p>模型返回的 <code>AssistantMessage.content</code> 里，可能有多个 <code>type === &quot;toolCall&quot;</code> 的块（模型一口气要求了多个操作）。先过滤出来：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">const toolCalls = message.content.filter((c) =&gt; c.type === &quot;toolCall&quot;);
</code></pre>
<p>然后决定这批工具 <strong>并行还是串行</strong> 执行：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">if (config.toolExecution === &quot;sequential&quot; || hasSequentialToolCall) {
    return executeToolCallsSequential(...);   // 串行
}
return executeToolCallsParallel(...);         // 并行
</code></pre>
<p><strong>“一票否决”策略</strong> ：只要这批工具中有 <strong>任何一个</strong> 声明了 <code>executionMode: &quot;sequential&quot;</code> ，整批都串行。为什么这么保守？因为判断”哪些工具会冲突”很难——edit 和 edit 操作不同文件就安全吗？万一它们编辑的文件有依赖关系呢？所以 Pi 选择了”宁可多等，不可出错”。而 edit 工具内部还有第二道防线（ <code>withFileMutationQueue</code> ，对同一文件的编辑串行化），确保即使外层判断为并行，也不会互相覆盖。</p><p>两种执行模式的内部结构：</p><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">串行模式：
  ToolCall A: 准备 → 验证 → beforeHook → 执行 → afterHook → emit end
  ToolCall B: 准备 → 验证 → beforeHook → 执行 → afterHook → emit end
  （一个完全结束，才开始下一个）

并行模式（三阶段设计）：
  阶段1 - 准备（顺序）：  A 准备 → B 准备 → C 准备
      ↑ prepareToolCall 含验证和 beforeHook，必须顺序执行
  阶段2 - 执行（并行）：  A、B、C 同时执行（Promise.all）
      ↑ 只有 tool.execute 并行，省时间
  阶段3 - 事件（有序）：  end 按完成顺序发；result 按调用顺序发
      ↑ result 消息保持和 ToolCall 一致的顺序，LLM 收到的上下文才是正确的
</code></pre>
<p>注意并行模式的精妙之处： <strong>准备阶段始终顺序</strong> （因为验证和权限检查不能并行——万一 B 被拦截了，C 就不应该执行）， <strong>只有实际执行并行</strong> 。</p><p>每个工具的结果被包装成 <code>ToolResultMessage</code> ，追加到 <code>context</code> 和 <code>newMessages</code> ：</p><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">工具执行后：
  context.messages = [..., user2, assistantMessage, {
    role: &quot;toolResult&quot;, toolCallId: &quot;toolu_01&quot;, toolName: &quot;read&quot;,
    content: [{ text: &quot;文件内容...&quot; }], isError: false
  }]
</code></pre>
<p><strong>terminate 机制</strong> ：工具可以在返回结果中设置 <code>terminate: true</code> ，表示”我觉得不该继续了”。但 Loop 不会因为某一个工具喊停就停——它用的是 <code>every</code> 而非 <code>some</code> ： <strong>必须这批工具的全部结果都设置了 terminate 才真正退出</strong> 。这是保守策略：只要有一个工具还在正常工作，Loop 就不中断。</p><blockquote><p>工具执行的完整五步管道（prepareArguments → Schema 验证 → beforeToolCall → execute → afterToolCall）详见《第5章：工具系统》。</p></blockquote>
<hr/><h3 id="47---efturnend-----steering">4.7 【内核+叠加 · 步骤E~F】turn_end + 钩子 + 再次检查 steering</h3><p>Turn 的核心工作做完了（调模型 + 执行工具），接下来是收尾。收尾分四步，其中前两步是内核，后两步是 coding-agent 叠加的扩展点：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">// ① emit turn_end —— 通知外部&quot;这一轮结束了&quot;（内核）
await emit({ type: &quot;turn_end&quot;, message, toolResults });

// ② prepareNextTurn —— 给外部一个机会&quot;改装&quot;下一轮（叠加）
// 返回值可包含 context / model / thinkingLevel 三者之一的覆盖
const nextTurnSnapshot = await config.prepareNextTurn?.({...});
if (nextTurnSnapshot) {
    currentContext = nextTurnSnapshot.context ?? currentContext;
    config.model = nextTurnSnapshot.model ?? config.model;
    // thinkingLevel 也在此处覆盖（详见 agent-loop.ts 中 prepareNextTurn 处理逻辑）
}

// ③ shouldStopAfterTurn —— 外部判断是否该停了（叠加）
if (await config.shouldStopAfterTurn?.({...})) {
    await emit({ type: &quot;agent_end&quot;, messages: newMessages });
    return;
}

// ④ 再次检查 steering —— 有没有新的紧急消息？（叠加1）
pendingMessages = (await config.getSteeringMessages?.()) || [];
</code></pre>
<p><strong><code>prepareNextTurn</code></strong> —— 这是个容易被忽略但很强大的扩展点。它在每个 turn_end 之后、下一轮开始前被调用，允许外部 <strong>动态切换下一轮的配置</strong> 。具体例子：</p><pre class="language-plaintext lang-plaintext"><code class="language-plaintext lang-plaintext">场景：按任务复杂度切换模型

Turn 1: 用户问了一个简单问题 → 模型用 Haiku（快、便宜）
        turn_end → prepareNextTurn 检查到问题很简单
        → 返回 { model: haiku } → 下一轮继续用 Haiku

场景：中途发现任务变复杂了

Turn 1: 用户让&quot;重构这个模块&quot; → Haiku 开始读文件
        turn_end → prepareNextTurn 发现要改的文件很多
        → 返回 { model: opus } → 下一轮自动切到 Opus（强、贵）
</code></pre>
<p>除了换模型，它还能换 <code>context</code> （比如注入新的上下文信息）和 <code>thinkingLevel</code> （思考强度）。返回 <code>undefined</code> 表示”什么都不换，按原配置继续”。</p><p><strong><code>shouldStopAfterTurn</code></strong> —— 外部注入的停止判断。coding-agent 层会用它来检查上下文窗口是否快满了（快满了就停下来触发压缩），或者是否达到了最大 Turn 数限制。这是产品层的”安全阀”，最简 Loop 不需要它。</p><p><strong>④再次检查 steering</strong> —— 一圈内层循环结束，再看看 steering 队列有没有新积攒的紧急消息。如果有，下一圈继续跑。</p><hr/><h3 id="48-">4.8 回到循环顶部</h3><p>工具执行完且收尾结束，代码回到内层 while 循环的条件判断。§三已经详细讲了 <code>hasMoreToolCalls</code> 由 <code>toolCalls 数组长度 &gt; 0 &amp;&amp; !terminate</code> 驱动——这里只看代码：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">while (hasMoreToolCalls || pendingMessages.length &gt; 0)
</code></pre>
<p>两个条件任一为 true 就继续。 <code>hasMoreToolCalls</code> 由模型输出里有没有 toolCall 块（且未全部 terminate）决定（内核）， <code>pendingMessages</code> 由 steering 队列决定（叠加1）。两个都为 false 时，内层循环退出。</p><hr/><h3 id="49-2--gfollowup-">4.9 【叠加2 · 步骤G】外层循环：followUp 的续命机制</h3><p>内层循环退出了——没有工具要执行，也没有紧急消息。但 coding-agent 的设计是：此时检查一下 <code>getFollowUpMessages()</code> ，看看系统有没有追加任务：</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">const followUpMessages = (await config.getFollowUpMessages?.()) || [];
if (followUpMessages.length &gt; 0) {
    pendingMessages = followUpMessages;   // 塞进 pending，触发新 Turn
    continue;                              // 回到外层循环顶部
}
break;  // 两个队列都空了，真正退出
</code></pre>
<p>如果有 followUp 消息，它们被塞进 <code>pendingMessages</code> ， <code>continue</code> 回到外层循环顶部，内层循环检测到 <code>pendingMessages</code> 不为空， <strong>在同一个 Trace 内</strong> 继续跑。这比重新启动一个新的 <code>runAgentLoop()</code> 好在哪？——连续性：同一个 <code>newMessages</code> 数组、同一个事件序列，不需要额外合并。</p><p>§四开头已经说过：如果你做的是简单 Agent，外层循环是多余的——内层循环退出后直接 <code>break</code> + <code>emit agent_end</code> 就行了。</p><hr/><h3 id="410-steering-vs-followup">4.10 steering vs followUp：一张表看清两种干预</h3><p>本章出现了两种”外部消息注入”机制。虽然 §四开头已经分别介绍过，但放在一起对比更能看清它们的差异：</p><table><thead><tr><th> 维度 </th><th> steering（叠加1） </th><th> followUp（叠加2） </th></tr></thead><tbody><tr><td> <strong>检查时机</strong> </td><td> runLoop 开始前 + 内层循环 <strong>每圈</strong> 结尾 </td><td> 内层循环 <strong>全部结束</strong> 后 </td></tr><tr><td> <strong>语义</strong> </td><td> ”紧急插队”——在工具执行间隙中插入 </td><td> ”排队等叫号”——等当前任务全部完成 </td></tr><tr><td> <strong>典型场景</strong> </td><td> 用户在 Agent 工作时输入了新指令 </td><td> 系统在 Agent 完成后追加”顺便跑个测试” </td></tr></tbody></table><p>生活类比：steering 是你正在开会，有人敲门递了张纸条——“紧急，先看这个”。followUp 是开完会翻了翻信箱——“不急，但需要处理”。</p><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" role="img" aria-label="steering vs followUp 对比" data-src="https://dg-ai-notes.pages.dev/assets/260702-ch03-steering-vs-followup.svg"><rect width="960" height="540" fill="#faf7f2"></rect><line x1="480" y1="20" x2="480" y2="520" stroke="rgba(28,25,23,0.15)" stroke-width="0.8" stroke-dasharray="4,4"></line><rect x="40" y="40" width="400" height="56" rx="6" fill="rgba(181,82,58,0.06)" stroke="#b5523a" stroke-width="1"></rect><text x="240" y="64" fill="#b5523a" font-size="11" font-weight="600" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle" letter-spacing="0.14em">STEERING · 紧急插队</text><text x="240" y="84" fill="#57534e" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">生活类比：开会时有人敲门递纸条 </text><text x="60" y="140" fill="#1c1917" font-size="10" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif">循环时间线</text><line x1="60" y1="148" x2="440" y2="148" stroke="#1c1917" stroke-width="1"></line><rect x="80" y="138" width="80" height="20" rx="2" fill="rgba(28,25,23,0.05)" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"></rect><text x="120" y="152" fill="#1c1917" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">Turn 1</text><rect x="180" y="138" width="80" height="20" rx="2" fill="rgba(28,25,23,0.05)" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"></rect><text x="220" y="152" fill="#1c1917" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">Turn 2</text><rect x="280" y="138" width="80" height="20" rx="2" fill="rgba(181,82,58,0.10)" stroke="#b5523a" stroke-width="1"></rect><text x="320" y="152" fill="#b5523a" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">Turn 3</text><line x1="240" y1="158" x2="240" y2="220" stroke="#b5523a" stroke-width="1.2" stroke-dasharray="3,2"></line><rect x="200" y="220" width="80" height="32" rx="4" fill="rgba(181,82,58,0.06)" stroke="#b5523a" stroke-width="1"></rect><text x="240" y="240" fill="#b5523a" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">steering 入队</text><line x1="260" y1="158" x2="260" y2="178" stroke="#b5523a" stroke-width="0.8"></line><text x="262" y="172" fill="#b5523a" font-size="8" font-family="&#x27;Geist Mono&#x27;, monospace">每圈检查</text><text x="240" y="284" fill="#1c1917" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">→ 在 Turn 3 开头注入 </text><text x="240" y="298" fill="#57534e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">（当前 Turn 正常完成后注入，不打断执行） </text><rect x="60" y="340" width="360" height="160" rx="6" fill="rgba(181,82,58,0.04)" stroke="rgba(181,82,58,0.30)" stroke-width="1" stroke-dasharray="4,4"></rect><text x="80" y="364" fill="#b5523a" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.14em">CHARACTERISTICS</text><text x="80" y="388" fill="#1c1917" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">时机：每圈内层循环开头 + 结尾</text><text x="80" y="408" fill="#1c1917" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">来源：用户实时输入的新指令</text><text x="80" y="428" fill="#1c1917" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">影响：会驱动循环继续（不等当前完成）</text><text x="80" y="460" fill="#57534e" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace">getSteeringMessages()</text><text x="80" y="480" fill="#a8a29e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" font-style="italic">用户让 Agent 修 bug，中途想起&quot;也检查测试文件&quot; </text><rect x="520" y="40" width="400" height="56" rx="6" fill="rgba(45,110,60,0.06)" stroke="#2d6e3c" stroke-width="1"></rect><text x="720" y="64" fill="#2d6e3c" font-size="11" font-weight="600" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle" letter-spacing="0.14em">FOLLOWUP · 排队等叫号</text><text x="720" y="84" fill="#57534e" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">生活类比：开完会翻信箱 </text><text x="540" y="140" fill="#1c1917" font-size="10" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif">循环时间线</text><line x1="540" y1="148" x2="920" y2="148" stroke="#1c1917" stroke-width="1"></line><rect x="560" y="138" width="80" height="20" rx="2" fill="rgba(28,25,23,0.05)" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"></rect><text x="600" y="152" fill="#1c1917" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">Turn 1</text><rect x="660" y="138" width="80" height="20" rx="2" fill="rgba(28,25,23,0.05)" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"></rect><text x="700" y="152" fill="#1c1917" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">Turn 2</text><rect x="760" y="138" width="80" height="20" rx="2" fill="rgba(28,25,23,0.05)" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"></rect><text x="800" y="152" fill="#1c1917" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">Turn 3 (停)</text><line x1="800" y1="158" x2="800" y2="200" stroke="#57534e" stroke-width="1"></line><text x="812" y="180" fill="#57534e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif">内层循环结束</text><rect x="760" y="200" width="120" height="36" rx="4" fill="rgba(45,110,60,0.06)" stroke="#2d6e3c" stroke-width="1"></rect><text x="820" y="220" fill="#2d6e3c" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">检查 followUp</text><text x="820" y="232" fill="#2d6e3c" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">队列</text><line x1="820" y1="236" x2="820" y2="270" stroke="#2d6e3c" stroke-width="1.2" stroke-dasharray="3,2"></line><rect x="760" y="270" width="120" height="20" rx="2" fill="rgba(45,110,60,0.08)" stroke="#2d6e3c" stroke-width="1"></rect><text x="820" y="284" fill="#2d6e3c" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">新 Turn（续命）</text><text x="720" y="304" fill="#1c1917" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">→ 在同一个 Trace 内继续跑 </text><rect x="540" y="340" width="360" height="160" rx="6" fill="rgba(45,110,60,0.04)" stroke="rgba(45,110,60,0.30)" stroke-width="1" stroke-dasharray="4,4"></rect><text x="560" y="364" fill="#2d6e3c" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.14em">CHARACTERISTICS</text><text x="560" y="388" fill="#1c1917" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">时机：内层循环全部结束后</text><text x="560" y="408" fill="#1c1917" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">来源：系统追加（如&quot;顺便跑测试&quot;）</text><text x="560" y="428" fill="#1c1917" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">影响：触发外层循环，内层循环重开</text><text x="560" y="460" fill="#57534e" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace">getFollowUpMessages()</text><text x="560" y="480" fill="#a8a29e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" font-style="italic">Agent 完成任务后，系统追加&quot;顺便跑个测试&quot;</text></svg><p>steering vs followUp 对比</p><p><strong>配图说明</strong> ：左红右绿对照——steering 在每圈内层循环开头+结尾检查、紧急插队；followUp 在内层循环全部结束后检查、续命重启。底部列出各自时机/来源/影响/典型场景。</p><hr/><h2 id="loop-">五、总结：Loop 的四条核心设计</h2><p>回顾从按下回车到 Agent 完成的整段旅程：</p><h3 id="1-react-">1. ReAct 循环模式</h3><p>Loop 的本质是 Reason（模型思考）→ Act（执行工具）→ Observe（观察结果）→ Reason（再次思考）的循环。模型的输出内容决定”该调什么工具”——这是 Agent 区别于 Workflow 的核心。但”什么时候停”不是模型的决策，而是我们人类定义的规则： <strong>模型不再输出工具调用时，就认为本轮结束。</strong></p><h3 id="2-stopreason-">2. stopReason 驱动机制</h3><p>整个循环由一个字段驱动： <code>stopReason</code> 。但实际驱动循环的不是 stopReason 本身，而是模型输出里有没有 toolCall 块且未全部 terminate—— <code>&quot;有 toolCall&quot;</code> = 继续转；没有 toolCall 或全部 terminate = 准备停（或立即停）。 <strong>“是否继续”不靠代码的复杂判断，只靠一条简单规则：模型的输出中有没有工具调用。</strong> 这是 Agent 架构的核心设计原则——把决策外包给模型输出模式，代码只做最简单的信号判断。</p><h3 id="3---">3. 内核 + 叠加的架构思路</h3><p>Agent Loop 的内核极其简短——十几行代码就能实现一个能用的循环。Pi 的 coding-agent 在内核上叠加了 steering（紧急插队）、followUp（任务追加）、prepareNextTurn（动态切模型）、shouldStopAfterTurn（安全阀）等设计。 <strong>内核是所有 Agent 的通用法则，叠加是产品功能的按需选择。</strong> 做自己的 Agent 时，先搭内核，再按场景加叠加。</p><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600" role="img" aria-label="Agent Loop 内核与叠加设计" data-src="https://dg-ai-notes.pages.dev/assets/260702-ch03-kernel-onion.svg"><defs><marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#57534e"></polygon></marker></defs><rect width="960" height="600" fill="#faf7f2"></rect><rect x="40" y="40" width="640" height="520" rx="12" fill="rgba(28,25,23,0.02)" stroke="rgba(28,25,23,0.20)" stroke-width="1"></rect><text x="60" y="68" fill="#57534e" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.14em">+<p> 钩子（产品功能）</p></text><rect x="100" y="100" width="520" height="400" rx="10" fill="rgba(28,25,23,0.03)" stroke="rgba(28,25,23,0.25)" stroke-width="1"></rect><text x="120" y="128" fill="#57534e" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.14em">+<p> followUp（任务追加）</p></text><rect x="160" y="160" width="400" height="280" rx="8" fill="rgba(28,25,23,0.04)" stroke="rgba(28,25,23,0.30)" stroke-width="1"></rect><text x="180" y="188" fill="#57534e" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.14em">+<p> steering（紧急插队）</p></text><rect x="220" y="220" width="280" height="160" rx="6" fill="rgba(181,82,58,0.08)" stroke="#b5523a" stroke-width="1.4"></rect><rect x="232" y="232" width="56" height="14" rx="2" fill="rgba(181,82,58,0.10)" stroke="rgba(181,82,58,0.50)" stroke-width="0.8"></rect><text x="260" y="243" fill="#b5523a" font-size="7" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle" letter-spacing="0.08em">KERNEL</text><text x="360" y="276" fill="#1c1917" font-size="14" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">最简 Loop（~10 行）</text><text x="360" y="298" fill="#57534e" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace" text-anchor="middle">while (hasMoreToolCalls)</text><text x="360" y="316" fill="#57534e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">调模型 → 检查 stopReason</text><text x="360" y="332" fill="#57534e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle">→ 执行工具 → 再调模型</text><text x="360" y="364" fill="#b5523a" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" text-anchor="middle" font-style="italic">所有 Agent 都需要的内核</text><line x1="700" y1="100" x2="700" y2="500" stroke="rgba(28,25,23,0.10)" stroke-width="0.8"></line><g transform="translate(720, 100)"><rect x="0" y="0" width="14" height="14" rx="2" fill="rgba(181,82,58,0.10)" stroke="#b5523a" stroke-width="1"></rect><text x="22" y="12" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif">内核</text><text x="0" y="32" fill="#57534e" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">最简 Loop：调模型 +</text><text x="0" y="46" fill="#57534e" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">执行工具 + stopReason</text><text x="0" y="62" fill="#a8a29e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" font-style="italic">通用法则，所有 Agent 都需要</text> </g><g transform="translate(720, 200)"><rect x="0" y="0" width="14" height="14" rx="2" fill="rgba(28,25,23,0.04)" stroke="rgba(28,25,23,0.30)" stroke-width="1"></rect><text x="22" y="12" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif">+<p> steering</p></text><text x="0" y="32" fill="#57534e" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">紧急消息插队，</text><text x="0" y="46" fill="#57534e" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">用户在工作中补充指令</text><text x="0" y="62" fill="#a8a29e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" font-style="italic">交互式产品的真实需求</text> </g><g transform="translate(720, 300)"><rect x="0" y="0" width="14" height="14" rx="2" fill="rgba(28,25,23,0.03)" stroke="rgba(28,25,23,0.25)" stroke-width="1"></rect><text x="22" y="12" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif">+<p> followUp</p></text><text x="0" y="32" fill="#57534e" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">任务追加续命，</text><text x="0" y="46" fill="#57534e" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">Agent 完成后顺势追加</text><text x="0" y="62" fill="#a8a29e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" font-style="italic">同一个 Trace 内继续跑</text> </g><g transform="translate(720, 400)"><rect x="0" y="0" width="14" height="14" rx="2" fill="rgba(28,25,23,0.02)" stroke="rgba(28,25,23,0.20)" stroke-width="1"></rect><text x="22" y="12" fill="#1c1917" font-size="11" font-weight="600" font-family="&#x27;Geist&#x27;, sans-serif">+<p> 钩子</p></text><text x="0" y="32" fill="#57534e" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">prepareNextTurn（切模型）</text><text x="0" y="46" fill="#57534e" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif">shouldStopAfterTurn（安全阀）</text><text x="0" y="62" fill="#a8a29e" font-size="9" font-family="&#x27;Geist&#x27;, sans-serif" font-style="italic">外部扩展点，按需注入</text> </g><line x1="40" y1="568" x2="920" y2="568" stroke="rgba(28,25,23,0.10)" stroke-width="0.8"></line><text x="40" y="586" fill="#57534e" font-size="10" font-family="&#x27;Geist Mono&#x27;, monospace" letter-spacing="0.14em">INSIGHT</text><text x="120" y="586" fill="#1c1917" font-size="10" font-family="&#x27;Geist&#x27;, sans-serif" font-style="italic">剥掉所有叠加层，最简 Loop 仍能正常工作。这是判断&quot;内核是否被污染&quot;的试金石。</text></svg><p>Agent Loop 内核与叠加设计</p><p><strong>配图说明</strong> ：从里到外的四层洋葱——最内核是最简 Loop（~10 行通用法则）；外层依次叠加 steering（紧急插队）、followUp（任务追加）、钩子（切模型/安全阀）。剥掉任何一层，里层仍能运行。</p></div></div>]]></description><link>https://binzi.top/posts/ai/Pi%20Agent%20loop</link><guid isPermaLink="true">https://binzi.top/posts/ai/Pi%20Agent%20loop</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 04 Aug 2026 15:47:00 GMT</pubDate></item><item><title><![CDATA[Pi Agent 上下文工程，模型每一轮到底看到了什么]]></title><description><![CDATA[<link rel="preload" as="image" href="https://binzi.top/img/covers/pi-agent-context-defense.svg"/><link rel="preload" as="image" href="https://binzi.top/img/covers/pi-agent-context-pipeline.svg"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/agent/Pi%20Agent%20%E4%B8%8A%E4%B8%8B%E6%96%87%E5%B7%A5%E7%A8%8B">https://binzi.top/posts/agent/Pi%20Agent%20%E4%B8%8A%E4%B8%8B%E6%96%87%E5%B7%A5%E7%A8%8B</a></blockquote><div><p>Pi Agent 给人的一个错觉是，它好像记得整个项目。</p><p>你让它读文件、跑测试、修改实现，再回头问它为什么这样改，它通常还能接上前面的工作。但打开源码会发现，Pi 并没有给模型一块无限记忆。每次调用模型之前，它都会从会话、工具结果、项目规则和当前输入里，重新裁出一份可以发送的消息。</p><p>所以真正值得研究的问题不是模型能记住多少，而是这一轮请求发出之前，Pi 决定让模型看到什么，又把什么留在了上下文之外。</p><p>这就是上下文工程。</p><p><img src="https://binzi.top/img/covers/pi-agent-context-defense.svg" alt="Pi Agent 上下文工程的四层防线" height="0" width="0"/></p><p>上图可以先当作全文地图。工具结果要控制体积，项目规则要按层级注入，过长的线性历史要压缩，会话树切换分支时还要保留旧分支的探索成果。它们共同处理的不是同一个问题，也不应该被塞进一个巨大的 <code>summarizeHistory</code> 函数里。</p><h2 id="">一、先把“上下文”拆成两次变换</h2><p>在 <code>@earendil-works/pi-agent-core</code> 中，Agent 内部先处理自己的 <code>AgentMessage[]</code>，然后才把它转换成模型 API 能理解的消息。</p><pre class="language-text lang-text"><code class="language-text lang-text">AgentMessage[]
    ↓ transformContext()
AgentMessage[]
    ↓ convertToLlm()
LLM Message[]
    ↓
模型 API
</code></pre>
<p><code>transformContext</code> 是一个可选的上下文变换点，可以裁剪消息，也可以在调用模型前注入额外信息。
<code>convertToLlm</code> 则负责把 Agent 内部的消息转换成模型协议。</p><p>Pi 的 Agent 内部可以保存自定义消息，但模型通常只直接理解 <code>user</code>、<code>assistant</code> 和 <code>toolResult</code> 这类标准消息，因此转换层还要过滤或改写模型不认识的内容。</p><p>coding-agent 在这条核心链路之上继续加入系统提示词、工具定义、项目上下文文件、Skills、Session Tree 和工具执行结果。完整一点可以写成这样。</p><pre class="language-text lang-text"><code class="language-text lang-text">Session Tree + 当前输入 + system prompt + tools
                         ↓
                   AgentMessage[]
                         ↓
                 transformContext()
                         ↓
                   convertToLlm()
                         ↓
                    LLM Message[]
</code></pre>
<p>这带来一个很重要的判断。</p><p>模型每次看到的上下文，不等于 Session 里保存的所有消息，也不等于终端里显示的全部内容。它是一个在请求前生成的投影。后面讲工具截断和 Compaction 时，都应该记住这个边界。</p><h2 id="">二、单次工具结果，先在入口处设上限</h2><p>上下文变长有两个来源。一个是历史越积越多，另一个是某一次工具调用就返回了过大的结果。</p>
<table><thead><tr><th> 限制        </th><th style="text-align:right">    默认值 </th><th> 作用                    </th></tr></thead><tbody><tr><td> 最大行数      </td><td style="text-align:right"> 2000 行 </td><td> 防止输出行数过多              </td></tr><tr><td> 最大字节数     </td><td style="text-align:right">   50KB </td><td> 防止少量超长行占满窗口           </td></tr><tr><td> grep 单行长度 </td><td style="text-align:right"> 500 字符 </td><td> 防止压缩文件的一行命中吃掉大量 token </td></tr></tbody></table><p>只限制行数不够，因为压缩后的 JavaScript 可能一行就有几十 KB。只限制字节数也不理想，因为源代码应该尽量按完整行保留，直接在任意字节处切开，结果会变得很难读。因此 PI 采用双重限制，行数或字节数任意一个先到，就停止继续保留。</p><h3 id="read--bash-">read 和 bash 为什么从不同方向截断</h3><p>Pi 并不是所有输出都使用同一种截断策略。</p><p><code>read</code> 读取文件时，开头的 import、类型定义和类接口通常更有助于理解文件结构，因此更适合使用 <code>truncateHead</code>，保留文件开头。</p><p><code>bash</code> 执行命令时，编译错误、测试失败和最终统计往往出现在末尾，因此更适合使用 <code>truncateTail</code>，保留末尾一段。</p><p>把尾部截断的核心逻辑压缩成伪代码，大概是这样。</p><pre class="language-ts lang-ts"><code class="language-ts lang-ts">function truncateTail(
  content: string,
  maxLines = 2000,
  maxBytes = 50 * 1024,
) {
  const lines = content.split(&quot;\n&quot;);
  const kept: string[] = [];
  let bytes = 0;

  for (let index = lines.length - 1; index &gt;= 0; index -= 1) {
    const line = lines[index];
    const lineBytes = byteLength(line) + 1;

    if (kept.length &gt;= maxLines || bytes + lineBytes &gt; maxBytes) {
      break;
    }

    kept.unshift(line);
    bytes += lineBytes;
  }

  return kept.join(&quot;\n&quot;);
}
</code></pre>
<p>这里还有几个工程细节。</p><ul><li>UTF-8 字符不能按任意字节切开，中文和 emoji 都要保持完整。</li><li>一行本身超过 50KB 时，不能因为它太长就返回空结果，需要保留可读的部分并标记为部分截断。</li><li>截断不能是静默丢弃，结果里应当告诉模型原始输出有多大、当前展示了哪一段，以及完整内容是否落在临时文件中。</li></ul><p>关键在于，默认只发送高信号片段，同时给模型一条继续获取完整内容的路径，远比让模型在一开始就吞下整份日志稳妥。</p><h3 id="2000">两个“2000”不是一回事</h3><p><code>truncate.ts</code> 里的 2000 是正常工具结果的最大行数。</p><p>Compaction 使用 <code>serializeConversation</code> 准备摘要请求时，又会把每个 <code>toolResult</code> 限制在 2000 个字符左右，并在文本里留下被截断的标记。</p><p>前者控制工具结果如何进入普通 Agent Loop，后者控制历史如何进入摘要模型。它们都叫 2000，却处于两条不同的链路上。做上下文预算时，不能只看到一个数字就认为全局已经有了统一限制。</p><h2 id="">三、系统提示词不是一段固定字符串</h2><p>工具输出截断是在做减法，系统提示词组装则是在做有选择的加法。</p><p>Agent 需要知道项目规范，但不应该每轮都把整个项目资料库塞进 system prompt。Pi 的做法是把必须知道的规则自动装进提示词，把可能用到的长资料留给工具按需读取。</p><h3 id="">上下文文件按目录层级合并</h3><p><code>loadProjectContextFiles</code> 会读取用户级 <code>agentDir</code> 下的上下文文件，再从当前工作目录向父目录查找项目规则。<code>loadContextFileFromDir</code> 当前识别 <code>AGENTS.md</code>、<code>AGENTS.MD</code>、<code>CLAUDE.md</code> 和 <code>CLAUDE.MD</code> 这些文件名变体。</p><p>一个典型的目录关系可以写成这样。</p><pre class="language-text lang-text"><code class="language-text lang-text">/myorg
├── AGENTS.md                         组织级规则
└── teams
    └── team-a
        ├── CLAUDE.md                 团队规则
        └── projects
            └── app-a
                ├── AGENTS.md         项目规则
                └── src/              当前工作目录
</code></pre>
<p>这样组织之后，上层目录可以放通用约束，项目目录再补充具体规则。源码会把不同层级的内容按通用到具体的顺序整理出来，让靠近当前项目的说明拥有更明确的上下文位置。</p><p>在 system prompt 中，它们还会被包在清晰的结构边界里。</p><pre class="language-xml lang-xml"><code class="language-xml lang-xml">&lt;project_context&gt;
  &lt;project_instructions path=&quot;/myorg/AGENTS.md&quot;&gt;
    组织级规则
  &lt;/project_instructions&gt;

  &lt;project_instructions path=&quot;/myorg/teams/team-a/projects/app-a/AGENTS.md&quot;&gt;
    项目规则
  &lt;/project_instructions&gt;
&lt;/project_context&gt;
</code></pre>
<p>XML 标签在这里承担的是边界标记。模型能看到这段内容来自哪个文件，也能区分项目指令和普通对话。对规则类文本来说，来源和范围都比把几段 Markdown 直接拼起来更重要。</p><h3 id="skills-">Skills 只放目录，正文按需加载</h3><p>Skills 是另一类容易把 system prompt 撑大的内容。一个 Skill 可能包含完整的操作流程，但一次任务通常只会用到其中一部分。</p><p><code>formatSkillsForPrompt</code> 放入 system prompt 的主要是下面三项。</p><table><thead><tr><th> 内容       </th><th> 用途                </th></tr></thead><tbody><tr><td> Skill 名称 </td><td> 让模型知道有哪些能力        </td></tr><tr><td> Skill 描述 </td><td> 帮模型判断是否匹配当前任务     </td></tr><tr><td> Skill 路径 </td><td> 匹配后通过 <code>read</code> 加载全文 </td></tr></tbody></table><p>这是一种目录和正文分离的设计。系统不必提前猜模型会用到哪份长文档，只要先告诉它可用能力和读取位置。任务展开后，模型再把真正需要的内容拉进当前上下文。</p><p>但懒加载不是越多越好。每轮都必须遵守的规则应该直接放入 system prompt，否则模型可能根本不会主动读取。只有低频、任务相关、篇幅较长的知识，才适合做成按需加载的 Skill。</p><h2 id="session-tree-">四、Session Tree 不是聊天列表</h2><p>到了历史管理阶段，Pi 的一个关键选择会变得很明显。Session 不是一条只能向后追加的聊天列表，而是一棵可以从不同叶子节点继续工作的会话树。</p><pre class="language-text lang-text"><code class="language-text lang-text">SessionHeader
├── type: &quot;message&quot;         user / assistant / toolResult
├── type: &quot;compaction&quot;
└── type: &quot;branch_summary&quot;
</code></pre>
<p>当前请求的上下文，通常由根节点到当前叶子的路径构建出来。用户从旧节点分叉时，路径就会改变。Compaction 和 Branch Summary 也不是只存在于内存里的两段 prompt 文本，而是会作为条目持久化在 Session Tree 中，再由上下文构建逻辑转换成模型可读的 summary message。</p><p>这个区别很容易被忽略。</p><p><code>CompactionEntry</code> 和运行时的 <code>CompactionSummaryMessage</code> 不是同一个层次。前者是会话格式里的持久化记录，后者是为了下一次模型调用而生成的消息表示。分支摘要也是一样，Session 中保存 <code>BranchSummaryEntry</code>，构建上下文时再把它转换成模型能理解的内容。</p><h2 id="compaction-">五、Compaction 如何压缩线性历史</h2><p>当用户让 Agent 连续搜索代码、修改实现、跑测试、根据报错继续修改时，早期消息会逐渐成为上下文负担。Pi 用 Compaction 处理这条线性历史。</p><h3 id="">触发条件和保留范围</h3><p>自动压缩的判断可以概括为。</p><pre class="language-text lang-text"><code class="language-text lang-text">contextTokens &gt; contextWindow - reserveTokens
</code></pre>
<p>当前官方文档给出的默认值是 <code>reserveTokens = 16384</code>，<code>keepRecentTokens = 20000</code>。它们可以在 <code>~/.pi/agent/settings.json</code> 或项目内的 <code>.pi/settings.json</code> 中配置。用户也可以通过 <code>/compact [instructions]</code> 手动触发，溢出重试则属于另一种 <code>overflow</code> 原因。</p><p>这两个参数分别解决不同的问题。</p><p><code>reserveTokens</code> 给压缩请求和后续工作留出空间，避免已经贴着窗口上限运行。<code>keepRecentTokens</code> 决定压缩后保留多少最近消息，让当前正在进行的工作不被过早总结掉。</p><h3 id="pi-">Pi 如何选择切分点</h3><p>触发后，Pi 会从最新消息向前回溯，直到找到大致覆盖 <code>keepRecentTokens</code> 的范围，然后把更早的消息交给摘要请求。</p><p>它通常会在 turn 边界切分。一个 turn 从用户消息开始，到下一条用户消息前结束。这样可以尽量避免只保留半个工具调用链，导致模型看到工具调用却看不到对应结果。</p><p>如果单个 turn 自己就大到超过保留预算，源码还会允许在 assistant 消息处做更细的切分，并生成 turn-prefix summary。工具调用和工具结果需要成对保留，切分点不会落在孤立的 tool result 上。</p><h3 id="">摘要保存什么</h3><p>Pi 当前使用的结构化摘要包含六个部分。</p><pre class="language-text lang-text"><code class="language-text lang-text">## Goal
## Constraints &amp; Preferences
## Progress
## Key Decisions
## Next Steps
## Critical Context
</code></pre>
<p>对于 coding-agent，<code>Progress</code> 不能只写“已经处理了一些文件”。它应该能继续回答这些问题。</p><ul><li>哪些文件已经读过</li><li>哪些修改已经完成</li><li>哪些测试已经运行</li><li>哪个方案验证失败以及失败原因</li><li>下一步应该从哪里继续</li></ul><p>摘要末尾还会记录 <code>&lt;read-files&gt;</code> 和 <code>&lt;modified-files&gt;</code>。这些信息会和摘要的 <code>details</code> 一起进入后续状态恢复，避免模型压缩后重新猜测工作现场。</p><p>摘要请求还会把上一轮摘要作为迭代上下文。这样多次 Compaction 时，新的摘要不是只看最近一小段原始消息，而是可以在旧摘要的基础上继续整理。</p><h3 id="">压缩条目不会把历史“抹掉”</h3><p>压缩完成后，Pi 会在 Session Tree 中追加一个 <code>CompactionEntry</code>。条目里至少有摘要内容、<code>firstKeptEntryId</code> 和 <code>tokensBefore</code>，还可以包含本次摘要的 usage 与文件操作详情。</p><p>构建下一次 LLM 请求时，Pi 从这个条目恢复摘要，再接上 <code>firstKeptEntryId</code> 之后仍然保留的消息。模型看到的是“摘要加近期尾部”，而不是所有旧消息，也不是一条没有来源的孤立总结。</p><p>这使得 Compaction 既能控制 token，又能保留会话格式中的可追溯边界。</p><h2 id="branch-summarization-">Branch Summarization 如何保留失败探索</h2><p>Compaction 处理一条线上的历史，分支摘要处理的是会话树中的另一种遗忘。</p><pre class="language-text lang-text"><code class="language-text lang-text">root
├── 方案 A
│   └── 测试发现性能不行，放弃
└── 方案 B
    └── 当前正在继续
</code></pre>
<p>用户从 <code>方案 A</code> 的旧节点切换到 <code>方案 B</code>，当前路径通常只包含 <code>root → 方案 B</code>。如果完全丢掉旧分支，模型可能再次提出已经验证失败的方案 A。</p><p>Pi 在 <code>/tree</code> 导航到另一条分支时，可以生成 Branch Summary。实现过程可以拆成三步。</p><ol start="1"><li>找到新旧路径的最近公共祖先，也就是 LCA。</li><li>收集 LCA 之后旧分支上的消息，并优先准备较新的内容进入摘要预算。</li><li>生成摘要，将它作为 <code>BranchSummaryEntry</code> 追加到当前会话路径。</li></ol><p>它和 Compaction 的差异可以放在一张表里。</p><table><thead><tr><th> 维度 </th><th> Compaction </th><th> Branch Summarization </th></tr></thead><tbody><tr><td> 触发 </td><td> 接近窗口上限、手动 <code>/compact</code> 或溢出重试 </td><td> <code>/tree</code> 切换会话分支 </td></tr><tr><td> 目标 </td><td> 让线性历史继续装得下 </td><td> 带回旧分支的探索成果 </td></tr><tr><td> 选取范围 </td><td> 历史中的可切分前缀 </td><td> LCA 之后的旧分支路径 </td></tr><tr><td> 持久化条目 </td><td> <code>CompactionEntry</code> </td><td> <code>BranchSummaryEntry</code> </td></tr><tr><td> 摘要格式 </td><td> 结构化摘要 </td><td> 使用同一套结构化摘要 </td></tr></tbody></table><p>这里有一个需要特别修正的细节。Branch Summary 不是另一套固定的五段模板，也不应当被简单理解成固定 2048 token 的摘要。当前 Pi 官方文档说明，Compaction 和 Branch Summarization 共用同一套结构化摘要格式，具体预算会根据待总结内容和上下文空间准备。</p><p>分支摘要同样会携带 <code>&lt;read-files&gt;</code> 和 <code>&lt;modified-files&gt;</code>。Pi 会把工具调用和之前摘要里的文件记录累积起来，避免切换分支后丢掉已经读过或修改过的文件线索。</p><h2 id="">从一次工具调用看完整链路</h2><p><img src="https://binzi.top/img/covers/pi-agent-context-pipeline.svg" alt="一次 Pi Agent 工具调用的上下文处理链路"/></p><p>假设用户输入的是“修复 auth.ts 的 bug”，一次典型流程大致如下。</p><pre class="language-text lang-text"><code class="language-text lang-text">用户输入
  ↓
buildSystemPrompt
  ├── 读取全局与项目上下文文件
  ├── 拼接工具描述和 guidelines
  └── 放入 Skills 清单
  ↓
Session Tree 根到当前叶子的路径
  ↓
AgentMessage[]
  ↓ transformContext()
按扩展或运行时策略裁剪、注入
  ↓ convertToLlm()
过滤和转换为模型消息
  ↓
LLM 返回 read(&quot;auth.ts&quot;)
  ↓
执行 read 工具
  ↓ truncateHead
最多 2000 行、50KB
  ↓
toolResult 回到 AgentMessage 历史
  ↓
下一次请求前检查上下文预算
  ├── 未到阈值，继续 Agent Loop
  └── 到达阈值，追加 CompactionEntry
</code></pre>
<p>如果用户中途切换会话树分支，Pi 会在新的路径上追加 <code>BranchSummaryEntry</code>。运行时上下文构建逻辑再把这些持久化条目转换成模型看到的摘要消息。</p><p>这条链路里，每一层回答的问题不同。</p><table><thead><tr><th> 层 </th><th> 解决的问题 </th></tr></thead><tbody><tr><td> 工具截断 </td><td> 单条结果太大怎么办 </td></tr><tr><td> System prompt 组装 </td><td> 项目规则如何自动进入请求 </td></tr><tr><td> <code>transformContext</code> / <code>convertToLlm</code> </td><td> 内部消息如何变成模型消息 </td></tr><tr><td> Compaction </td><td> 线性历史太长怎么办 </td></tr><tr><td> Branch Summary </td><td> 旧分支的探索如何不丢 </td></tr></tbody></table><p>把 <code>reserveTokens</code> 调大，解决不了一条 80KB 的工具结果。把工具输出截得很小，也不会自动告诉模型项目必须使用哪条测试命令。每一层都做窄一点，整个 Agent 才容易观察和调整。</p><h2 id="">三个设计判断</h2><h3 id="1-">1. 上下文是每轮重建的投影，不是聊天记录的别名</h3><p>Session 负责保存可恢复的历史，Agent Context 负责为当前调用选择路径和消息，LLM Message 则是最终发送格式。三者之间存在转换关系。</p><p>因此，调试 Agent 失忆时，不能只盯着终端里是否显示过某段文本，还要确认这段文本有没有进入当前叶子路径，是否在 <code>transformContext</code> 中被裁掉，是否能通过 <code>convertToLlm</code>，以及最终是否被工具结果截断或 Compaction 摘要覆盖。</p><p>这比把问题归结成“模型记性不好”更接近真实故障边界。</p><h3 id="2-">2. 压缩必须保留工作状态，而不只是保留聊天主题</h3><p>普通聊天摘要可以只保留讨论结论，coding-agent 不行。它还要保留文件关系、修改状态、测试结果和失败尝试。</p><p>Pi 把 <code>readFiles</code> 和 <code>modifiedFiles</code> 放进摘要详情，并在分支摘要中继续累积。这说明上下文压缩的目标不是把过去讲得更短，而是让下一次调用可以继续操作同一个工作现场。</p><p>如果自己设计摘要模板，<code>Goal</code> 很重要，但 <code>Progress</code>、<code>Key Decisions</code>、<code>Next Steps</code> 和文件操作记录通常更决定恢复质量。</p><h3 id="3-">3. 摘要应该是扩展点，而不是黑盒</h3><p>Pi 提供 <code>session_before_compact</code> 和 <code>session_before_tree</code> 这类扩展事件。扩展可以查看待总结消息、上一轮摘要、文件操作、触发原因和 token 信息，也可以取消默认行为或返回自定义摘要。</p><p>这给了应用层一个机会。不同项目可能有不同的状态载体，数据库迁移、前端页面、数据分析 notebook 需要保留的字段并不相同。统一的默认摘要负责兜底，领域扩展负责补充真正影响后续工作的状态。</p><h2 id="">上下文工程不等于权限控制</h2><p>还有一个边界需要单独说清楚。</p><p>系统提示词可以告诉模型哪些文件不要改，Skills 也可以描述一套操作规范，但这些内容本身不是操作系统级的权限边界。Pi 的官方 README 明确说明，它不内置权限系统，进程会使用启动它的用户权限运行。</p><p>因此，上下文工程解决的是模型看什么、记什么、何时读取什么。需要限制文件访问、命令执行或网络能力时，还要使用沙箱、容器、进程权限和工具白名单。把一条规则写进 prompt，不能替代真正的安全隔离。</p>
<h2 id="">总结</h2><p>Pi Agent 的上下文工程可以概括为一条从原始事件到模型消息的流水线。</p><p>工具结果先被限制体积，系统提示词把项目规则按来源和层级组织起来，Session Tree 提供当前工作路径，<code>transformContext</code> 和 <code>convertToLlm</code> 再把内部消息变成模型协议。历史接近窗口上限时，Compaction 保存线性工作的状态；切换会话分支时，Branch Summary 带回旧探索的结论。</p><p>模型不需要记住一切，但当前这一轮必须看到最有用的那部分。Pi 的实现值得研究的地方，也正在这里。它没有把上下文当作一个越堆越大的文本框，而是把上下文变成了可以截断、加载、压缩、持久化和重新投影的工程对象。</p></div></div>]]></description><link>https://binzi.top/posts/agent/Pi%20Agent%20%E4%B8%8A%E4%B8%8B%E6%96%87%E5%B7%A5%E7%A8%8B</link><guid isPermaLink="true">https://binzi.top/posts/agent/Pi%20Agent%20%E4%B8%8A%E4%B8%8B%E6%96%87%E5%B7%A5%E7%A8%8B</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sun, 02 Aug 2026 04:00:00 GMT</pubDate></item><item><title><![CDATA[Pi Agent 工具架构设计：三层类型 + 五步管道 + 并行调度 + 错误处理]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/agent/Pi%20Agent%20%E5%B7%A5%E5%85%B7%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1">https://binzi.top/posts/agent/Pi%20Agent%20%E5%B7%A5%E5%85%B7%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1</a></blockquote><div><p>在 Pi Agent 的工具系统中，工具的抽象和执行机制是整个架构的核心。本文系统梳理工具定义的三层类型、调用时的五步管道、并行执行策略，以及错误处理的特殊设计。</p><h2 id="">一、三层类型：为什么”一个工具”要分三层来定义？</h2><p>在 Pi Agent 里，一个完整的工具定义被拆成了<strong>三层</strong>，目的是让不同层职责清晰、扩展性强：</p><h3 id="tool">第一层：Tool（一张”名片”）</h3><p>这是最基础的一层，只定义工具的<strong>基本信息</strong>：</p><ul><li>toolName</li><li>description</li><li>parameters（Schema）</li><li>其他元数据</li></ul><p>这一层就像工具的<strong>名片</strong>，告诉 Agent 它能做什么。</p><pre class="language-ts lang-ts"><code class="language-ts lang-ts">export interface Tool {
  name: string;
  description: string;
  parameters: any;
  // ...
}
</code></pre>
<h3 id="agenttool">第二层：AgentTool（加上了”执行能力”）</h3><p>在 Tool 的基础上，添加了<strong>实际执行逻辑</strong>：</p><ul><li>execute 方法</li><li>错误处理</li><li>资源管理等</li></ul><pre class="language-ts lang-ts"><code class="language-ts lang-ts">interface AgentTool extends Tool {
  execute: (params: any) =&gt; Promise&lt;any&gt;;
}
</code></pre>
<p>这一层是真正可以调用的工具。</p><h3 id="tooldefinition">第三层：ToolDefinition（产品层再加东西）</h3><p>在 AgentTool 之上，添加了<strong>产品层</strong>需要的额外信息：</p><ul><li>包装后的执行函数</li><li>前置/后置钩子</li><li>结果统一格式</li></ul><pre class="language-ts lang-ts"><code class="language-ts lang-ts">interface ToolDefinition extends AgentTool {
  // 包装后的执行函数
  wrappedExecute: (params: any) =&gt; Promise&lt;ToolResult&gt;;
}
</code></pre>
<h2 id="wraptooldefinition">桥接两层：wrapToolDefinition</h2><p>为了把第二层的执行能力桥接到第三层，Pi 提供了 <code>wrapToolDefinition</code> 工具：</p><pre class="language-ts lang-ts"><code class="language-ts lang-ts">function wrapToolDefinition(tool: AgentTool): ToolDefinition {
  return {
    ...tool,
    wrappedExecute: async (params) =&gt; {
      // 执行前置钩子
      await beforeToolCall?.(params);
      
      try {
        const result = await tool.execute(params);
        return { result, isError: false };
      } catch (err) {
        return { result: err, isError: true };
      }
    }
  };
}
</code></pre>
<h2 id="">为什么非要分三层？</h2><ul><li><strong>职责分离</strong>：第一层只管描述，第二层管执行，第三层管包装和扩展。</li><li><strong>扩展性强</strong>：想加新特性（日志、限流、缓存）时，只需要在第三层修改。</li><li><strong>可组合性好</strong>：可以用不同的 <code>wrapToolDefinition</code> 实现不同的策略。</li></ul><h2 id="">二、五步管道</h2><p>模型调用工具时，不是直接调用一个函数，而是走了一套完整的<strong>五步管道</strong>：</p><h3 id="-1-preparearguments--">第 1 步：prepareArguments —— 兼容性垫片</h3><p>处理不同模型返回的参数格式（比如有些模型返回 camelCase，有些 snake_case）。</p><h3 id="-2-validatetoolarguments--schema-">第 2 步：validateToolArguments —— Schema 验证</h3><p>使用 JSON Schema 对参数进行严格验证，防止无效输入。</p><h3 id="-3-beforetoolcall--">第 3 步：beforeToolCall —— 前置钩子（可阻止执行）</h3><p>允许开发者在执行前做一些操作（比如权限检查、限流），也可以通过返回 <code>false</code> 来阻止执行。</p><h3 id="-4-toolexecute--">第 4 步：tool.execute —— 实际执行</h3><p>真正调用工具的执行逻辑（通常是第二层的 <code>execute</code>）。</p><h3 id="-5-aftertoolcall--">第 5 步：afterToolCall —— 后置钩子（可修改结果）</h3><p>允许开发者在执行后修改结果、添加额外信息等。</p><p><strong>管道终点</strong>：<code>ToolResultMessage</code>（统一格式的消息）。</p><p>这种五步管道让工具调用更加安全、可控、可扩展。</p><h2 id="-vs-">三、并行 vs 串行：一个批次的工具不是”一起跑就完了”</h2><p>模型经常一次调用多个工具（比如搜索 + 总结 + 数据库查询），但并行执行不能简单用 <code>Promise.all</code>。</p><p>Pi 的调度策略是<strong>一票否决</strong>：</p><ul><li>并行执行三阶段设计：
<ol start="1"><li>准备阶段（prepare）</li><li>并行执行阶段（execute in parallel）</li><li>收尾阶段（after parallel）</li></ol></li></ul><p>如果任何一个工具执行失败，会触发一票否决，整个批次被取消并返回错误。</p><h2 id="">四、永不抛出：工具出错也是一条消息</h2><p>Pi Agent 工具系统<strong>永不抛出异常</strong>到 Agent Loop，而是统一返回 <code>ToolResult</code>：</p><pre class="language-ts lang-ts"><code class="language-ts lang-ts">interface ToolResult {
  result: any;
  isError: boolean;
  errorMessage?: string;
}
</code></pre>
<h3 id="6---1-">6 种错误类型 + 1 种产物</h3><p>工具出错时，会被包装成统一的 <code>ToolResult</code>，而不是直接抛出错误。</p><p>关键代码在 <code>tool.execute</code> 的双重防护：</p><ul><li>内部主动捕获异常</li><li>外部框架兜底包装成消息</li></ul><p><strong>为什么”伪装成消息”是最佳处理方式？</strong></p><p>因为：</p><ul><li>模型最擅长处理消息格式的错误</li><li>错误描述越具体，模型纠错能力越强</li><li>避免了异常穿透到 Agent Loop，保持 Loop 的稳定性</li></ul><h2 id="operations-">五、【进阶】Operations 抽象：工具执行不等于系统调用</h2><p>问题：如果工具直接写死系统调用（如 <code>fs.readFile</code>），那就很难在不同环境中复用。</p><p><strong>解法</strong>：让工具不直接调系统 API，而是通过<strong>最小接口</strong>调用。</p><p>每个工具定义自己需要的最小接口（比如 <code>ReadOperations</code>），然后通过依赖注入的方式传入具体实现。</p><p>这样：</p><ul><li>工具代码可以跨环境复用</li><li>Mock 测试变得容易</li><li>不同 Workspace 切换时行为稳定</li></ul><hr/><p><strong>总结</strong></p><p>Pi Agent 的工具架构通过<strong>三层定义 + 五步管道 + 并行调度 + 统一错误消息 + Operations 抽象</strong>，实现了高度灵活、可测试、可扩展的工具系统。</p><p>这套设计真正做到了把 Agent 的手脚管住。</p></div></div>]]></description><link>https://binzi.top/posts/agent/Pi%20Agent%20%E5%B7%A5%E5%85%B7%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1</link><guid isPermaLink="true">https://binzi.top/posts/agent/Pi%20Agent%20%E5%B7%A5%E5%85%B7%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 01 Aug 2026 09:05:00 GMT</pubDate></item><item><title><![CDATA[Pi Agent 工具架构设计：Operations 抽象与 Workspace 切换]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/agent/Pi%20Agent%20%E5%B7%A5%E5%85%B7%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%EF%BC%9AOperations">https://binzi.top/posts/agent/Pi%20Agent%20%E5%B7%A5%E5%85%B7%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%EF%BC%9AOperations</a></blockquote><div><p>在 Pi Agent 的工具系统中，<strong>Operations 抽象</strong> 是核心设计之一。它把工具的执行逻辑与具体文件系统实现解耦，让 Agent 可以在本地、SSH、Docker 等不同环境中灵活切换。</p><p>本文主要分享工具架构的核心设计思路，包括抽象接口、依赖注入、闭包固定实例，以及 Workspace 切换机制。</p><h2 id="-nodejs-fs">为什么不直接使用 Node.js fs？</h2><p>很多人第一反应是直接调用 <code>fs/promises</code>：</p><pre class="language-ts lang-ts"><code class="language-ts lang-ts">await fs.access(path);
const buffer = await fs.readFile(path);
</code></pre>
<p>这种写法存在几个明显痛点：</p><ul><li><strong>强耦合</strong>：Tool 与具体文件系统实现绑定（本地磁盘、SSH、Docker 沙箱随便一种都行）。</li><li><strong>测试困难</strong>：单元测试必须创建真实文件，Mock 成本高。</li><li><strong>环境切换麻烦</strong>：一旦需要切换后端，Tool 代码几乎需要全部重写。</li></ul><p>直观上可以理解为：</p><pre class="language-text lang-text"><code class="language-text lang-text">Read Tool
      ↓
Node.js fs
      ↓
本地磁盘
</code></pre>
<p>工具和具体实现绑定死了，这不是在给 Agent 装手脚，而是把手脚绑死在本地磁盘上了。</p><h2 id="operations-">Operations 抽象接口</h2><p>为了解决这个问题，我们定义了一个抽象接口 <code>ReadOperations</code>：</p><pre class="language-ts lang-ts"><code class="language-ts lang-ts">export interface ReadOperations {
    readFile: (absolutePath: string) =&gt; Promise&lt;Buffer&gt;;
    access: (absolutePath: string) =&gt; Promise&lt;void&gt;;
}
</code></pre>
<p>Tool 层只知道调用这个接口，完全不关心下面用的是 <code>fs.readFile</code> 还是 <code>sshClient.readFile</code>。</p><p>调用关系变为：</p><pre class="language-text lang-text"><code class="language-text lang-text">Read Tool
      │
      ▼
ReadOperations（抽象接口）
      │
      ▼
Local / SSH / Docker / Mock 等具体实现
</code></pre>
<p>Tool 依赖的是抽象接口，而不是具体实现。</p><h2 id="">闭包固定实例行为</h2><p>在创建 Tool 时，我们通常会这样写：</p><pre class="language-ts lang-ts"><code class="language-ts lang-ts">function createReadTool(options?: {
    operations?: ReadOperations;
}) {
    const ops = options?.operations ?? defaultReadOperations;

    return {
        execute: async (path: string) =&gt; {
            await ops.access(path);
            return ops.readFile(path);
        }
    };
}
</code></pre>
<p>这里用闭包把创建时的 <code>ops</code> 固定在 Tool 实例上，实现了<strong>行为稳定</strong>的目的。</p><p>不管后续怎么切换 Workspace，Tool 实例的行为永远是确定的。</p><h2 id="workspace-">Workspace 切换与多环境支持</h2><p>本地 Agent 用本地 Operations，SSH Agent 用 SSH Operations，切换 Workspace 时不需要修改 Tool 内部代码：</p><pre class="language-ts lang-ts"><code class="language-ts lang-ts">const localTool = createReadTool({ operations: localOperations });
const sshTool   = createReadTool({ operations: sshOperations });
</code></pre>
<p>两个 Tool 可以同时存在，互不干扰。</p><h2 id="">工具系统五步管道简述</h2><p>整个工具系统可大致分为五步：</p><ol start="1"><li><strong>定义</strong>：定义 Operations 接口</li><li><strong>注册</strong>：把具体实现注册到工具系统中</li><li><strong>拦截</strong>：可选的预处理/后处理</li><li><strong>执行</strong>：调用 Operations 实例的方法</li><li><strong>回收</strong>：清理资源或重置状态</li></ol><p>我们重点强化了第 4 步的抽象和第 5 步的稳定性。</p><h2 id="">两层错误处理机制</h2><p>工具执行时推荐采用<strong>两层错误处理</strong>：</p><ul><li><strong>第一层（工具内部主动识别）</strong>：在 <code>execute</code> 方法中主动捕获已知错误类型（如超时、中止、非零退出码），重新包装成具体、可读的错误描述。</li><li><strong>第二层（框架兜底被动处理）</strong>：工具内部抛出的错误原样透传给框架。</li></ul><p>这种做法能给模型提供更有价值的错误反馈。</p><h2 id="">总结：工具架构的核心思想</h2><p>Pi Agent 工具架构的核心在于<strong>先抽象、再注入、最后稳定</strong>。</p><p>通过 Operations 抽象 + 依赖注入 + 闭包固定实例的方式，我们实现了：</p><ul><li>环境切换的灵活性</li><li>测试的简易性</li><li>行为稳定性的保证</li></ul><p>这样的设计让 Tool 层代码可以在不同 Workspace 间复用，同时保持了清晰的职责边界。</p></div></div>]]></description><link>https://binzi.top/posts/agent/Pi%20Agent%20%E5%B7%A5%E5%85%B7%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%EF%BC%9AOperations</link><guid isPermaLink="true">https://binzi.top/posts/agent/Pi%20Agent%20%E5%B7%A5%E5%85%B7%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%EF%BC%9AOperations</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 01 Aug 2026 09:05:00 GMT</pubDate></item><item><title><![CDATA[AIO BIO NIO区别？]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/java/AIO%20BIO%20NIO%E5%8C%BA%E5%88%AB%EF%BC%9F">https://binzi.top/posts/java/AIO%20BIO%20NIO%E5%8C%BA%E5%88%AB%EF%BC%9F</a></blockquote><div><h1 id="aio-bio-nio">AIO BIO NIO区别？</h1><h3 id="bio-io"><strong>BIO（同步阻塞 I/O）</strong></h3><p>BIO 是传统的 I/O 模式，全称为 <strong>Block I/O</strong>，也叫同步阻塞 I/O。在 BIO 模式下，当我们执行 I/O 操作时，比如读取文件或者网络请求，程序会 <strong>阻塞</strong>，直到 I/O 操作完成后，才能继续执行后面的代码。也就是说，当一个线程在等待 I/O 完成时，它是处于<strong>阻塞状态</strong>的，不能做其他事情。</p><h3 id="nio-io"><strong>NIO（同步非阻塞 I/O）</strong></h3><p><strong>一个线程管理多个连接</strong></p><p>NIO 是对传统 BIO 的改进，它全称是 <strong>Non-blocking I/O</strong>（非阻塞 I/O）。在 NIO 模式下，I/O 操作不是阻塞的，可以同时处理多个 I/O 请求。与 BIO 不同的是，NIO 使用了 <strong><a href="https://zhida.zhihu.com/search?content_id=252965872&amp;content_type=Article&amp;match_order=1&amp;q=Channel&amp;zhida_source=entity">Channel</a>（通道）</strong> 和 <strong><a href="https://zhida.zhihu.com/search?content_id=252965872&amp;content_type=Article&amp;match_order=1&amp;q=Buffer&amp;zhida_source=entity">Buffer</a>（缓冲区）</strong> 来进行数据读写，并且它支持 <strong>多路复用</strong>，这意味着多个 I/O 操作可以共享一个线程来处理，从而大大提高了并发性能。</p><h3 id="aio-io"><strong>AIO（异步非阻塞 I/O）</strong></h3><p>AIO 是 NIO 的进一步升级，叫做 <strong>Asynchronous I/O</strong>（异步非阻塞 I/O）。在 AIO 模式下，I/O 操作是完全异步的，不需要线程阻塞等待 I/O 完成。AIO 基于事件和回调机制，当 I/O 操作完成时，系统会通知应用程序，程序就能接收到结果。</p></div></div>]]></description><link>https://binzi.top/posts/java/AIO%20BIO%20NIO%E5%8C%BA%E5%88%AB%EF%BC%9F</link><guid isPermaLink="true">https://binzi.top/posts/java/AIO%20BIO%20NIO%E5%8C%BA%E5%88%AB%EF%BC%9F</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Thu, 30 Jul 2026 09:32:00 GMT</pubDate></item><item><title><![CDATA[Kimi K3 发布的一些个人感受]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/K3%E5%8F%91%E5%B8%83%E7%9A%84%E4%B8%80%E4%BA%9B%E4%B8%AA%E4%BA%BA%E6%84%9F%E5%8F%97">https://binzi.top/posts/ai/K3%E5%8F%91%E5%B8%83%E7%9A%84%E4%B8%80%E4%BA%9B%E4%B8%AA%E4%BA%BA%E6%84%9F%E5%8F%97</a></blockquote><div><p>Kimi K3 的技术博客里，有一个很容易被忽略、但很关键的细节：</p><p>K3 对「思考历史」极度敏感。harness 如果没有完整传回之前的 thinking 内容，生成质量会严重不稳定。</p><p>原文大致写了两个触发条件：</p><ol start="1"><li>harness 没有完整回传历史 thinking 内容</li><li>会话中途从别的模型切换到 K3</li></ol><p>博客对原因也做了解释。大多数 reasoning 模型，比如 Claude 的 extended thinking、OpenAI 的 o 系列，默认假设是：thinking 是一次性草稿纸。生成完就完成使命了，下一轮不一定需要原样保留。有服务商在服务端加密保存，有的直接允许客户端丢弃。模型训练时，就没指望「未来轮次还能看到这段草稿」。</p><p>K3 不是这样训练的。</p><p>它是在「每一轮都把之前所有轮次的完整 thinking 摆在上下文里」这个条件下训练出来的。thinking 对 K3 不是草稿纸，而是这个模型认知链条里永久的一部分。它的行为模式，建立在「我永远能看到自己之前想了什么」这个假设之上。</p><h2 id="-thinking--tool-result-">为什么 thinking 不能像 tool result 那样裁</h2><p>这里有个很有意思的对比，可以类比 agent 上下文里的 tool result 裁剪。</p><p>tool result 之所以能被相对安全地裁剪，是因为它读完之后，语义会被显式表达进下一轮的可见回复里。原文没了，但结论已经「抄」到别处留下来了。</p><p>thinking 不是这样。</p><p>thinking block 里的推理过程，大部分时候不会被完整翻译进最终的可见回复。可见回复往往只是 thinking 里那条推理链的一个结论摘要。中间的探索、排除、纠错过程，只存在于 thinking 本身。</p><p>裁掉这部分，模型下一轮拿到的不是「浓缩后的知识」，而是真的丢了一段计算过程。它自己也不知道自己之前想过什么、排除过什么、纠正过什么。</p><p>这就是为什么后果是 “quality highly unstable”，而不是「效率下降」。</p><p>这不是 KV cache miss 那种成本问题，是模型拿到的信息本身就不完整了，是正确性问题。</p><h2 id="">从别的模型切过来，也不行</h2><p>第二个触发条件更有意思。</p><p>从别的模型切到 K3，即使把 thinking 内容原样传过去，质量也会不稳定。</p><p>因为 K3 训练时看到的历史 thinking，全都是它自己生成的、符合自己训练分布的推理风格。换一个模型生成的 thinking，哪怕格式一样，推理模式和措辞习惯都不同。K3 把这段陌生风格的历史当成自己的思考去续写，容易接不上。</p><h2 id="-harness-">对做 harness 的人意味着什么</h2><p>对做 agent harness 的人来说，这意味着两件事。</p><p>一是不能对 K3 用和别的模型一样的上下文压缩策略。thinking 这部分几乎不能裁。</p><p>二是模型路由天然跟 K3 冲突。简单任务用便宜模型、复杂步骤再切强模型，这种常见做法在 K3 上会出问题，因为它没有「接手一个陌生会话」的能力。</p><p>模型开源了，但用好它，几乎绑死了兼容的官方 harness。</p><p>这大概也是为什么官方会特别推荐 Kimi Code，并建议不要在会话中途切到 K3。</p><p>不是模型不够强，是它把「完整可见的自我思考历史」写进了训练假设里。这个假设一旦被 harness 破坏，表现就会立刻塌掉。</p></div></div>]]></description><link>https://binzi.top/posts/ai/K3%E5%8F%91%E5%B8%83%E7%9A%84%E4%B8%80%E4%BA%9B%E4%B8%AA%E4%BA%BA%E6%84%9F%E5%8F%97</link><guid isPermaLink="true">https://binzi.top/posts/ai/K3%E5%8F%91%E5%B8%83%E7%9A%84%E4%B8%80%E4%BA%9B%E4%B8%AA%E4%BA%BA%E6%84%9F%E5%8F%97</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 17 Jul 2026 10:33:45 GMT</pubDate></item><item><title><![CDATA[RRF 排序融合，混合检索如何统一 BM25 和向量搜索]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/RRF%E6%8E%92%E5%BA%8F%E8%9E%8D%E5%90%88">https://binzi.top/posts/ai/RRF%E6%8E%92%E5%BA%8F%E8%9E%8D%E5%90%88</a></blockquote><div><p>做混合检索时，会碰到一个很朴素的问题。</p><p>BM25 说某篇文档得了 18.6 分，向量检索说另一篇文档的相似度是 0.83。到底谁应该排在前面？</p><p>18.6 看起来比 0.83 大得多，但这两个数字根本不在同一把尺子上。直接相加没有意义，随手归一化又会受到查询、模型和数据分布影响。</p><p>RRF 提供了一个很省心的思路。</p><p>既然各路分数不好比较，那就不比较分数，只比较排名。</p><h2 id="">为什么混合检索需要结果融合</h2><p>关键词检索和向量检索擅长的事情并不一样。</p><p>BM25 根据词项出现情况计算相关性，遇到错误码、产品型号和专有名词时很有优势。查询里明确出现 <code>ERR_CONNECTION_RESET</code>，包含同一字符串的文档通常应该优先返回。</p><p>向量检索比较的是语义表示。用户问「服务突然连不上了怎么办」，即使文档里没有完全相同的句子，只写了「连接被远端重置」，它也有机会把内容召回。</p><p>两路结果放在一起，覆盖面通常比单一路径更好。麻烦在于，它们返回的原始分数没有统一含义。</p><p>BM25 分数会受到词频、文档长度和索引语料影响。向量检索还可能使用余弦相似度、点积或距离，不同模型生成的分数分布也不同。</p><p>给两路分数直接设置权重，看起来简单，后面却很容易陷入反复调参。数据规模变了，Embedding 模型换了，原来的权重可能就不合适了。</p><p>RRF 绕过了这道题。</p><blockquote><p>这篇是 <a href="https://binzi.top/2026/10/12/ai-agent-rag/">RAG 概念专题</a>中的排序细节，完整系列见 <a href="https://binzi.top/2026/10/12/ai-agent-concepts/">Ai Agent概念扫盲</a>。</p></blockquote>
<h2 id="rrf-">RRF 怎么计算</h2><p>RRF 的全称是 Reciprocal Rank Fusion，也就是倒数排名融合。</p><p>对于文档 <code>d</code>，它的融合分数可以写成下面这样。</p><p>$$<br/>score(d) = \sum<em>{i=1}^{n} \frac{1}{k + rank</em>i(d)}
$$</p><p><code>rank_i(d)</code> 表示文档 <code>d</code> 在第 <code>i</code> 路结果中的排名。排名从 1 开始。某篇文档没有出现在一路结果中时，那一路就不给它加分。</p><p><code>k</code> 是平滑常数，常见取值是 60。它会削弱第一名和后面名次之间过于悬殊的差距，让多路都排得靠前的文档得到更稳定的优势。这里的 <code>k</code> 与检索返回数量 Top K 不同；公式和具体参数可参考 <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">Elasticsearch 的 RRF 文档</a>。</p><p>这里最重要的变化是，原始相关性分数消失了。</p><p>无论 BM25 返回 18.6，还是向量库返回 0.83，RRF 只问一件事，这篇文档在各自列表里排第几。</p><h2 id="">手算一个小例子</h2><p>假设同一个问题得到两组结果。</p><p>BM25 的排序是 <code>文档 A、文档 B、文档 C</code>。</p><p>向量检索的排序是 <code>文档 B、文档 C、文档 A</code>。</p><p>为了让数字更容易观察，这里暂时取 <code>k = 0</code>。真实系统通常会使用更大的平滑常数。</p><p>文档 A 在两路中分别排第 1 和第 3，它的分数是</p><p>$$<br/>1/1 + 1/3 = 1.3333<br/>$$</p><p>文档 B 分别排第 2 和第 1，它的分数是</p><p>$$<br/>1/2 + 1/1 = 1.5<br/>$$</p><p>文档 C 分别排第 3 和第 2，它的分数是</p><p>$$<br/>1/3 + 1/2 = 0.8333<br/>$$</p><p>融合后的顺序是 <code>文档 B、文档 A、文档 C</code>。</p><p>文档 B 没有在 BM25 中拿到第一，但它在两路都很靠前，所以最终超过了只在一路占据第一的文档 A。这正是 RRF 想保留的共识。</p><h2 id="-python-">一个可以直接运行的 Python 实现</h2><p>RRF 本身并不依赖 LangChain 或向量数据库。只要手里已经有多组排序结果，几十行 Python 就能完成融合。</p><pre class="language-python lang-python"><code class="language-python lang-python">from collections import defaultdict

def reciprocal_rank_fusion(
    rankings: list[list[str]],
    k: int = 60,
) -&gt; list[tuple[str, float]]:
    &quot;&quot;&quot;Fuse ranked document lists using Reciprocal Rank Fusion.&quot;&quot;&quot;
    if k &lt; 0:
        raise ValueError(&quot;k must be greater than or equal to 0&quot;)

    scores: dict[str, float] = defaultdict(float)

    for ranking in rankings:
        seen_in_ranking: set[str] = set()
        for rank, document_id in enumerate(ranking, start=1):
            if document_id in seen_in_ranking:
                continue
            seen_in_ranking.add(document_id)
            scores[document_id] += 1 / (k + rank)

    return sorted(scores.items(), key=lambda item: (-item[1], item[0]))

bm25_results = [&quot;文档A&quot;, &quot;文档B&quot;, &quot;文档C&quot;]
vector_results = [&quot;文档B&quot;, &quot;文档C&quot;, &quot;文档A&quot;]

fused_results = reciprocal_rank_fusion(
    [bm25_results, vector_results],
    k=0,
)

for document_id, score in fused_results:
    print(f&quot;{document_id}: {score:.4f}&quot;)
</code></pre>
<p>运行后得到</p><pre class="language-text lang-text"><code class="language-text lang-text">文档B: 1.5000
文档A: 1.3333
文档C: 0.8333
</code></pre>
<p>代码里额外处理了同一文档在一路结果中重复出现的情况。正常的检索器通常不会返回重复 ID，但在合并多个分片或自行拼装结果时，去重可以避免同一路对某篇文档重复加分。</p><h2 id="-langchain--ensembleretriever">在 LangChain 里使用 EnsembleRetriever</h2><p>下面保留的是旧版 LangChain API 示例，用于理解组合方式，不表示当前版本安装后可直接运行。实际项目应按所用版本核对包名和导入路径；上面的独立 Python 实现不依赖 LangChain。</p><p>如果用 LangChain 搭混合检索，RRF 也经常不是自己手写，而是通过 <code>EnsembleRetriever</code> 这类组件完成。</p><p>一个典型结构是：先准备一批文档，再分别创建 BM25 检索器和向量检索器，最后把两路检索器交给 <code>EnsembleRetriever</code> 融合。</p><pre class="language-python lang-python"><code class="language-python lang-python">import jieba
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings

doc_list = [
    &quot;人工智能是计算机科学的一个分支，致力于创造能够模拟人类智能的系统。&quot;,
    &quot;机器学习是人工智能的一个子领域，专注于开发能从数据中学习的算法。&quot;,
    &quot;深度学习是机器学习的一种方法，使用多层神经网络处理复杂数据。&quot;,
    &quot;自然语言处理是人工智能的一个分支，研究计算机与人类语言的交互。&quot;,
    &quot;智能助手如小度和天猫精灵使用自然语言处理技术理解用户指令。&quot;,
    &quot;自动驾驶汽车利用计算机视觉和深度学习技术识别道路情况。&quot;,
    &quot;医疗诊断系统使用机器学习分析患者数据，辅助医生做出诊断决策。&quot;,
    &quot;大型语言模型如 GPT 和文心一言能够生成类似人类的文本内容。&quot;,
    &quot;计算机视觉技术使机器能够理解和处理视觉信息，如图像和视频。&quot;,
    &quot;强化学习是一种机器学习方法，通过奖励和惩罚机制让 AI 学会做决策。&quot;,
    &quot;人工智能产业在中国快速发展，成为新的经济增长点。&quot;,
    &quot;智能手机的普及改变了人们的生活和工作方式。&quot;,
]

def chinese_tokenizer(text):
    return list(jieba.lcut(text))

bm25_retriever = BM25Retriever.from_texts(
    doc_list,
    metadatas=[{&quot;source&quot;: f&quot;doc_{i}&quot;} for i in range(len(doc_list))],
    preprocess_func=chinese_tokenizer,
)
bm25_retriever.k = 3

embeddings = HuggingFaceEmbeddings(
    model_name=&quot;../bge-large-zh-v1.5&quot;,
    model_kwargs={&quot;device&quot;: &quot;cpu&quot;},
)

vectorstore = Chroma.from_texts(
    texts=doc_list,
    embedding=embeddings,
    metadatas=[{&quot;source&quot;: f&quot;doc_{i}&quot;} for i in range(len(doc_list))],
    persist_directory=&quot;./chroma_db_custom_3&quot;,
)
vector_retriever = vectorstore.as_retriever(search_kwargs={&quot;k&quot;: 3})

ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.5, 0.5],
)

docs = ensemble_retriever.invoke(&quot;人工智能在医疗领域的应用&quot;)
for index, doc in enumerate(docs, start=1):
    print(index, doc.page_content)
</code></pre>
<p>这里 BM25 更擅长直接关键词匹配，比如「深度学习」这种查询。向量检索更擅长语义相关但字面不完全一致的问题，比如「智能系统如何理解人类语言」。混合检索则希望把两者的优势放在一起。</p><p>原理上，<code>EnsembleRetriever</code> 会把不同检索器返回的结果做融合。它关心的是文档在各路结果中的排名，再结合权重得到最终排序。上面给 BM25 和向量检索都设置了 <code>0.5</code>，意思是先等权看待两路结果。</p><p>如果业务里有大量专有名词、编号和错误码，可以适当提高 BM25 的权重；如果查询更偏自然语言表达，可以提高向量检索的权重。但权重最好通过测试集评估，而不是只凭直觉。</p><p>测试时可以准备几类问题。</p><ul><li>直接关键词匹配：例如「深度学习」</li><li>语义相关但没有完全相同关键词：例如「智能系统如何理解人类语言」</li><li>混合查询：例如「人工智能在医疗领域的应用」</li><li>有歧义的查询：例如「智能产品」</li></ul><p>分别观察 BM25、向量检索和混合检索的结果，就能更直观看到 RRF 或加权融合到底改善了什么。</p><h2 id="k-"><code>k</code> 到底影响什么</h2><p>如果 <code>k = 0</code>，第一名得到 1 分，第二名得到 0.5 分，第十名只得到 0.1 分。头部名次之间差距很大。</p><p>如果 <code>k = 60</code>，第一名约为 0.01639，第二名约为 0.01613，第十名约为 0.01429。名次仍然重要，但变化平缓了很多。</p><p>较小的 <code>k</code> 更强调头部名次，较大的 <code>k</code> 更重视多路结果共同提供的信号。60 是常见默认值，不是不可修改的定律。更稳妥的做法，是在自己的查询集上用 Recall、MRR 或 NDCG 等指标评估。</p><h2 id="">多路权重怎么处理</h2><p>有些场景会更信任其中一路。例如知识库中包含大量编号和术语，BM25 的精确匹配可能比向量语义更重要。</p><p>可以给每一路增加权重。</p><p>$$<br/>score(d) = \sum<em>{i=1}^{n} \frac{w</em>i}{k + rank_i(d)}
$$</p><p>这样仍然绕开了原始分数尺度，只调整各路排名信号的重要程度。</p><p>不过权重越多，调参成本也会回来。没有离线评估集时，先从等权 RRF 开始，通常比凭感觉设置一堆系数更稳。</p><h2 id="rrf-">RRF 的边界</h2><p>RRF 简单、鲁棒，但它并不会判断一篇文档是否真的回答了问题。</p><p>如果关键词检索和向量检索都召回了错误内容，RRF 只会把这些错误结果重新排序。它也会丢掉原始分数中的置信度信息。某一路的第一名只比第二名高一点，和高出一大截，在 RRF 里没有区别。</p><p>所以它更适合做召回结果融合，而不是替代完整的相关性判断。</p><p>常见链路会是关键词召回与向量召回并行执行，用 RRF 合并候选，再把靠前的结果交给 Cross-Encoder 或大模型做精排。RRF 负责便宜、稳定地汇总多路意见，Reranker 再仔细判断查询和文档之间的相关性。</p><p>检索系统不需要一开始就追求复杂。</p><p>当两套评分体系无法直接对话时，先让它们各自投票，再根据名次寻找共识。</p><p>这就是 RRF 最实用的地方。</p></div></div>]]></description><link>https://binzi.top/posts/ai/RRF%E6%8E%92%E5%BA%8F%E8%9E%8D%E5%90%88</link><guid isPermaLink="true">https://binzi.top/posts/ai/RRF%E6%8E%92%E5%BA%8F%E8%9E%8D%E5%90%88</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 08 Jul 2026 17:04:11 GMT</pubDate></item><item><title><![CDATA[MCP 和 Skill，到底是什么关系？]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/MCP%E5%92%8CSkill%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88%E5%85%B3%E7%B3%BB">https://binzi.top/posts/ai/MCP%E5%92%8CSkill%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88%E5%85%B3%E7%B3%BB</a></blockquote><div><blockquote><p>这篇可以接着 <a href="https://binzi.top/2026/10/12/ai-agent-concepts/">Ai Agent概念扫盲</a> 阅读。想把 ACP、A2A、LSP 和 Code Mode 一起弄清楚，可以看 <a href="https://binzi.top/2026/10/12/ai-agent-tools-protocols/">工具与协议专题</a>。</p></blockquote>
<p>最近看 Agent 相关的项目，经常会同时碰到三个词，Function Calling、MCP 和 Skill。</p><p>它们看起来都和「让模型做事」有关，于是很容易混在一起。有人把 MCP 当成更高级的 Function Calling，也有人觉得有了 Skill 就不需要 MCP。</p><p>其实它们并不在同一层。</p><p>最简短的理解是，Function Calling 负责表达调用，MCP 负责接入能力，Skill 负责组织做事的方法。</p><p>一个让模型知道怎么开口，一个把工具接进来，一个告诉 Agent 事情应该怎么完成。</p><h2 id="function-calling-">Function Calling 解决的是调用格式</h2><p>大语言模型只能生成内容。哪怕它说自己要查询天气，也不会真的凭空发出网络请求。</p><p>应用需要先把可用函数的名称、用途和参数结构告诉模型。模型判断需要使用工具后，再返回一个结构化调用请求。宿主程序收到请求，执行真实函数，然后把结果放回对话。</p><p>一个简化后的工具定义可能长这样。</p><pre class="language-json lang-json"><code class="language-json lang-json">{
  &quot;name&quot;: &quot;get_weather&quot;,
  &quot;description&quot;: &quot;查询指定城市的天气&quot;,
  &quot;parameters&quot;: {
    &quot;type&quot;: &quot;object&quot;,
    &quot;properties&quot;: {
      &quot;city&quot;: { &quot;type&quot;: &quot;string&quot; }
    },
    &quot;required&quot;: [&quot;city&quot;]
  }
}
</code></pre>
<p>模型并没有亲自执行 <code>get_weather</code>。它只是在回答里表达「我想调用这个函数，参数是某个城市」。真正执行请求的，仍然是模型外面的程序。</p><p>所以 Function Calling 更像一种模型和程序之间的调用语言。它解决的是单次调用怎么表达，却没有规定工具该如何独立部署、怎样被发现，也不负责让同一套工具跨客户端复用。</p><p>工具只有两三个时，手动维护这些定义并不麻烦。等项目里出现几十个工具，问题就来了。每个应用都要保存一份 schema、路由代码和权限逻辑，工具接口一变，所有接入方还得跟着改。</p><p>MCP 处理的正是这一层麻烦。</p><h2 id="mcp-">MCP 解决的是能力接入</h2><p>MCP 是 Model Context Protocol。它把工具、数据和提示模板包装成遵循统一协议的服务，让支持 MCP 的客户端能够连接、发现并使用这些能力。</p><p>一套 MCP 架构里，通常能看到三个角色。</p><ul><li>Host 是承载 Agent 的应用，负责管理连接和安全边界</li><li>Client 位于 Host 内部，负责与某个 MCP Server 通信</li><li>Server 暴露具体能力，例如文件读取、数据库查询或 GitHub 操作</li></ul><p>Server 不需要了解上面连接的是哪一种模型。它只按照协议公布自己能提供什么，并响应 Client 发来的请求。</p><p>MCP Server 暴露的内容也不只有工具。</p><ul><li>Tools 用来执行操作，例如创建文件或调用接口</li><li>Resources 用来提供只读上下文，例如文档、日志和数据库记录</li><li>Prompts 用来提供可复用的提示模板</li></ul><p>它们通过 JSON-RPC 消息交互，可以运行在本地的 stdio 连接上，也可以通过 Streamable HTTP 提供远程服务。</p><p>当 Client 连接 Server 后，会先获取能力列表，再把可调用工具转换成模型能够理解的工具定义。模型仍然产生普通的工具调用，Client 负责把请求路由到对应 Server，拿到执行结果后再交还给模型。</p><p>从模型视角看，它未必知道 MCP 存在。模型只知道眼前有一批工具可以调用。自动发现、协议转换和请求路由，都是 Host 与 Client 在外面完成的。</p><p>因此 MCP 没有取代 Function Calling。很多实现里，它反而会把 MCP 工具转换成模型原生的工具调用格式。</p><p>Function Calling 是模型如何发出调用，MCP 是外部能力怎样标准化接进来。</p><p>MCP 减少的是各应用重复维护工具接入的负担。协议接通之后，仍然需要 Host、Client、Server 和模型调用机制配合，实际操作也需要对应业务权限。架构和角色可以参考 <a href="https://modelcontextprotocol.io/docs/learn/architecture">MCP 官方说明</a>。</p><h2 id="skill-">Skill 解决的是工作方法</h2><p>工具接进来以后，Agent 就一定能把事情做好吗？</p><p>不一定。</p><p>给一个 Agent 文件系统、终端和 GitHub 工具，只能说明它有能力读文件、跑命令、访问仓库。至于代码审查应该先看哪些文件、检查哪些风险、用什么格式交付，它仍然可能每次做得不一样。</p><p>Skill 处理的是这类问题。</p><p>一份 Skill 通常会包含名称、适用场景和详细执行说明，还可以附带脚本、参考资料与输出模板。它不是新的工具协议，更接近一份能够被 Agent 主动选择和执行的操作手册。</p><pre class="language-yaml lang-yaml"><code class="language-yaml lang-yaml">---
name: code-review
description: 审查代码修改，识别正确性、安全和性能问题
---

# 执行流程

1. 确认修改范围和预期行为
2. 阅读相关代码与测试
3. 运行针对性的检查
4. 按严重程度输出问题和证据
</code></pre>
<p>这份 Skill 没有直接提供读取文件的能力。它只规定审查应该怎样展开。真正读取文件时，Agent 仍然需要使用本地工具或 MCP Server 暴露的能力。Skill 中声明要使用某工具，也不会自动授予执行权限。格式与渐进式加载可以参考 <a href="https://agentskills.io/specification">Agent Skills 规范</a>。</p><p>所以可以把两者记成一句话。</p><p>MCP 让 Agent 有工具可用，Skill 让 Agent 知道怎样把工具组合起来完成任务。</p><h2 id="">三者怎样一起工作</h2><p>用一次代码审查就能把整条链路串起来。</p><p>用户要求 Agent 审查一批代码修改。Agent 先从可用 Skill 的名称和描述中，发现 <code>code-review</code> 与任务匹配，于是加载它的完整说明。</p><p>Skill 要求先确认修改范围。Agent 随后选择 GitHub MCP Server 提供的变更读取工具。模型通过 Function Calling 生成工具名和参数，MCP Client 把调用转发到 Server，再把返回的 diff 送回模型。</p><p>接下来，Skill 要求检查测试。Agent 又通过文件系统工具读取相关测试文件，并调用终端运行测试命令。</p><p>等证据收集完毕，Agent 按 Skill 规定的严重程度和格式输出报告。</p><p>这一次任务里，三者各自做了不同的事。</p><ul><li>Function Calling 表达每一次工具调用</li><li>MCP 负责发现、连接和路由外部能力</li><li>Skill 决定完整任务的步骤、标准与交付形式</li></ul><p>少一个都还能运行，但完成度会不一样。只有 Function Calling，工具接入容易散落在各个项目里。只有 MCP，Agent 有很多工具，却不一定有稳定的方法。只有 Skill，流程写得再清楚，没有真实工具也做不了事。</p><h2 id="skill-">Skill 为什么要渐进式加载</h2><p>Skill 还能携带脚本、模板和参考文档。如果 Agent 启动时把所有内容一次性塞进上下文，很快就会浪费大量 token，真正的任务信息反而被挤到角落。</p><p>更合理的方式是逐层加载。</p><p>Agent 启动时只看到每个 Skill 的名称和简介。任务到来后，它只加载匹配 Skill 的完整说明。执行到确实需要某份模板、脚本或参考资料时，再读取对应文件。</p><p>这有点像查手册。开始时只看目录，确定需要哪一章后再读正文，遇到具体表格时才翻到附录。不是知道得越多越好，而是在正确的时刻拿到正确的信息。</p><p>这个设计看起来只是节省上下文，实际上还在减少干扰。无关规则越少，Agent 越容易把注意力放在当前任务上。</p><p>Skill 真正解决的是「重复贴 prompt」的问题。</p><p>比如每次让 AI 做代码审查，都要重新说明检查安全漏洞、性能问题、边界条件和输出格式。一个人复制粘贴还勉强能忍，团队里每个人都手写一份标准时，质量就会开始漂移：有人漏掉安全检查，有人格式不统一，有人还在使用旧版规范。</p><p>Skill 把这些反复使用的指令、流程、脚本和模板放进一个可维护的文件夹里。一个成熟的 Skill 通常不只是几句提示词，而是有明确的结构。</p><pre class="language-text lang-text"><code class="language-text lang-text">code-review/
├── SKILL.md
├── scripts/
│   └── check_security.py
├── references/
│   └── review_standards.md
└── assets/
    └── report_template.md
</code></pre>
<p><code>SKILL.md</code> 是核心说明，顶部用 frontmatter 写名称和描述，正文写执行步骤。<code>scripts</code> 可以放自动化脚本，<code>references</code> 放参考资料，<code>assets</code> 放报告模板或其他资源。</p><p>这样 Agent 不只是拿到一句「帮我审查代码」，而是拿到一整套可复用的工作方法。需要读文件时，它可以调用 MCP 或本地文件工具；需要安全扫描时，它可以运行 Skill 自带脚本；需要交付结果时，它可以套用 Skill 里的模板。</p><p>所以「MCP + Skill」经常会一起出现：MCP 让 Agent 有手，Skill 让 Agent 有操作手册。</p><h2 id="">实际接入时是什么感觉</h2><p>我自己理解 MCP，最直观的一点是：它把原来散落在项目里的工具对接，变成了「配置一个 Server，然后让客户端发现它」。</p><p>比如在 Claude Desktop 里配置文件系统或 GitHub MCP Server，通常是在配置文件里加上 Server 的启动方式和参数。客户端启动后会连接这些 Server，读取它们暴露的工具列表，然后在对话中按需调用。</p><p>开发者不需要在每个对话里手写「read_file 这个函数怎么定义」「GitHub issue 这个接口怎么传参」。这些能力由 MCP Server 标准化提供，Host 负责发现和接入，模型只看到最终可用的工具。</p><p>这就是 MCP 和普通 Function Calling 最大的区别：Function Calling 更像一次调用的格式，MCP 更像工具服务的注册、发现和复用机制。</p><h2 id="">再把边界说清楚</h2><p>理解这三个概念，关键不是背定义，而是看清它们分别负责哪一段链路。</p><p>Function Calling 位于模型与宿主程序之间，让调用能够被结构化表达。</p><p>MCP 位于应用与外部能力之间，让工具和数据能够被发现、连接与复用。</p><p>Skill 位于任务与执行过程之间，把经验沉淀成可以重复使用的工作方法。</p><p>它们不是三个互相争位置的方案，而是一套 Agent 系统里可以同时存在的三层能力。</p><p>模型会不会调用，工具能不能接入，任务做得是否稳定。</p><p>这三件事，终于可以分开看了。</p></div></div>]]></description><link>https://binzi.top/posts/ai/MCP%E5%92%8CSkill%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88%E5%85%B3%E7%B3%BB</link><guid isPermaLink="true">https://binzi.top/posts/ai/MCP%E5%92%8CSkill%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88%E5%85%B3%E7%B3%BB</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 14 Apr 2026 07:05:43 GMT</pubDate></item><item><title><![CDATA[怎么减少大模型幻觉？]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/ai/%E6%80%8E%E4%B9%88%E5%87%8F%E5%B0%91%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%B9%BB%E8%A7%89">https://binzi.top/posts/ai/%E6%80%8E%E4%B9%88%E5%87%8F%E5%B0%91%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%B9%BB%E8%A7%89</a></blockquote><div><blockquote><p>本文侧重错误治理。完整的资料准备、分块、Embedding、BM25、重排和 GraphRAG，可以先读 <a href="https://binzi.top/2026/10/12/ai-agent-rag/">RAG 概念专题</a>；整个系列入口是 <a href="https://binzi.top/2026/10/12/ai-agent-concepts/">Ai Agent概念扫盲</a>。</p></blockquote>
<p>大模型最麻烦的时刻，不是它说不知道。</p><p>而是它给出一个语气笃定、结构完整、甚至带着出处的答案，等你真的去查，才发现内容根本不存在。</p><p>这种现象通常被叫作幻觉。</p><p>模型不是在主观撒谎。它的核心任务是根据上下文预测接下来最可能出现的内容。当可靠信息不足时，它仍然可能生成语言上非常合理的答案。读起来顺，不等于事实正确。</p><p>所以减少幻觉，不能只靠一句「请不要胡编」。更有效的思路是让模型有依据可用，在依据不足时允许拒答，并让最终结论能够被检查。</p><h2 id="">普通使用时，先缩小回答空间</h2><p>问题越开放，模型自由发挥的空间越大。</p><p>直接问「介绍一下这个项目」，模型可能自行补充作者、时间线和功能。把要求改成「只根据下面提供的 README，总结项目功能，材料没有提到的信息请标为未知」，回答边界会清楚很多。</p><p>一个实用的提示可以包含三件事。</p><pre class="language-text lang-text"><code class="language-text lang-text">只根据我提供的资料回答。
资料没有覆盖的内容，请明确说不知道，不要补充猜测。
每个关键结论后标出对应的资料片段或编号。
</code></pre>
<p>它不能保证模型绝不出错，但能把「自由生成」变成「有边界的整理」。</p><p>还可以要求模型把事实、推断和建议分开。事实必须来自材料，推断要说明推理过程，建议则明确标注为建议。三类内容混在一起时，读者很容易把模型的推测当成已经发生的事实。</p><h2 id="">要求来源，但不要盲信来源</h2><p>让模型给出处是个好习惯，不过这里还有一个坑。</p><p>模型可能连引用也一起编出来。一个看起来很像论文标题的字符串，一组格式规整的作者与年份，都不能自动成为可信证据。</p><p>真正有用的是可访问、可定位、能支持当前结论的来源。看到链接后，还要确认页面是否存在、原文是否真的表达了同一件事，以及信息是否已经过时。</p><p>医疗、法律、金融和生产环境操作等高风险问题，更不能只看答案写得像不像。至少应该回到官方文档、法规原文或权威机构材料核对。</p><p>语言模型很擅长生成「像答案的文本」，验证环节负责判断它是不是答案。</p><h2 id="rag-">RAG 能降低幻觉，但不会自动消灭幻觉</h2><p>在应用系统里，常见方案是 RAG，也就是检索增强生成。</p><p>用户提出问题后，系统先从知识库中检索相关内容，再把这些内容与问题一起交给模型。模型不必只靠训练阶段记住的知识回答，而是可以参考当前资料。</p><p>这对私有知识和频繁更新的信息很有帮助。产品文档更新后，只要重新建立索引，就能让后续回答使用新版本内容，不需要为了每一次文档变化重新训练模型。</p><p>可问题并没有到这里结束。</p><p>RAG 自己也会产生新的失败路径。</p><p>检索器可能没有找到真正相关的文档，也可能召回一堆表面相似但回答不了问题的内容。文档找对了，模型还可能脱离材料继续推断。材料之间互相冲突时，模型也可能悄悄选择其中一份，却不告诉用户冲突存在。</p><p>于是系统治理需要覆盖整条链路，而不只是 Prompt。</p><h2 id="">第一道防线是检索质量</h2><p>如果交给模型的上下文本身就是错的，后面写再严格的指令也很难救回来。</p><p>知识库需要选择合适的切分方式。块太小，完整语义会被切断。块太大，又容易混入很多无关信息。关键词检索适合精确术语，向量检索擅长语义相似，两者可以组合召回，再通过 <a href="https://binzi.top/2026/07/09/RRF%E6%8E%92%E5%BA%8F%E8%9E%8D%E5%90%88/">RRF 一类方法</a>融合结果。</p><p>召回之后，还可以使用 Reranker 对查询和候选文档做更细的相关性判断。检索指标也应该单独评估，例如观察正确资料是否进入 TopK，而不是只看模型生成的文字是否顺畅。</p><p>这一步不够显眼，却决定了模型手里到底拿到了什么。</p><h2 id="">第二道防线是低置信度拒答</h2><p>很多系统最大的错误，是无论检索结果多差，都要求模型必须回答。</p><p>如果多个结果无法覆盖问题，系统可以直接说明资料不足。经过验证的相关性阈值也可以作为信号，但相似度或重排分数不自动等于答案正确概率。拒答是在表达证据边界。</p><p>阈值也不能拍脑袋设置。太高会让系统频繁拒绝本来能回答的问题，太低又拦不住无关材料。可以准备一组包含可回答和不可回答问题的验证集，观察不同阈值下的误答率与拒答率，再选择能够接受的平衡点。</p><p>允许模型说不知道，往往比教它把每句话写得更肯定重要。</p><h2 id="">第三道防线是生成约束</h2><p>检索结果合格后，Prompt 仍然需要给出清楚边界。</p><p>可以要求模型只使用提供的上下文，资料不足时返回固定状态，并让每条结论携带来源编号。结构化输出也能让程序更容易检查结果。</p><pre class="language-json lang-json"><code class="language-json lang-json">{
  &quot;answer&quot;: &quot;根据资料生成的回答&quot;,
  &quot;citations&quot;: [&quot;doc-3#chunk-8&quot;],
  &quot;supported&quot;: true
}
</code></pre>
<p>这里的 <code>supported</code> 不该完全听模型自我判断。系统仍然需要检查引用 ID 是否存在、引用片段是否进入过上下文，并在必要时验证结论与证据是否一致。</p><p>Prompt 是约束，不是安全边界。真正稳定的系统要把能用程序检查的部分留给程序。</p><h2 id="">第四道防线是引用核查</h2><p>答案生成后，可以逐条检查关键声明。</p><p>最基础的检查，是确认每个引用都对应真实 chunk。再往前一步，可以把单条结论与引用片段交给一个核查模型，判断证据是否支持结论。如果证据只覆盖一半，就删除未被支持的部分，或者把答案降级成不确定表述。</p><p>核查模型也可能犯错，所以它更适合当第二道筛网，而不是绝对裁判。对特别重要的操作，还可以要求人工确认后再执行。</p><p>一个成本更低的策略，是只核查日期、数字、名称、因果关系和操作指令等高风险声明。没必要给每一句自然语言都增加一次模型调用。</p><p>如果把这些方法收束成工程实践，我会优先看四个方向。</p><p>第一是 Prompt 强约束。明确告诉模型只能根据提供的资料回答，资料里没有就说不知道，不要为了完整性补充猜测。</p><p>第二是检索质量门控。Rerank 分数或相关性分数低于阈值时，系统直接拒答，避免模型在低质量上下文上硬撑。</p><p>第三是生成后引用核查。每个关键声明都要能在 chunk 里找到来源依据，找不到就删除、降级或标为不确定。</p><p>第四是结构化输出强制溯源。比如让模型输出 JSON，每条结论附上来源编号，方便程序继续检查引用是否存在、证据是否进入过上下文。</p><p>作为一个实现起点，可以先做 Prompt 强约束和经过验证的检索质量门控。前者减少模型自由发挥，后者减少系统把低质量材料交给模型的概率。实际成本、效果和需要的核查层次，仍要通过自己的问题集验证。</p><h2 id="">评估时不要只看答案好不好读</h2><p>生成质量很容易被语言流畅度迷惑。</p><p>一个更实用的测试集，应该同时包含知识库中有答案的问题、只有部分答案的问题、完全没有答案的问题，以及资料彼此冲突的问题。</p><p>然后分别观察几件事。</p><ul><li>正确资料有没有被召回</li><li>没有证据时系统会不会拒答</li><li>答案中的声明能否在引用里找到</li><li>引用是否真的支持对应声明</li><li>更新知识库后，旧答案是否及时失效</li></ul><p>只有把检索、生成和引用拆开测，才知道错误到底发生在哪里。否则看到一个错误答案，很难判断是检索没找到、排序排错了，还是模型拿到正确资料后仍然编了内容。</p><h2 id="">没有一种办法能把幻觉降到零</h2><p>提示词约束有用，RAG 有用，Reranker 和引用核查也有用。但它们都不是「安装以后永不出错」的开关。</p><p>更可靠的系统，往往不是让模型显得无所不知，而是认真处理不知道的时刻。</p><p>有资料就引用资料，证据不足就降低语气，超出边界就明确拒答，高风险结论再交给人确认。</p><p>模型负责生成，系统负责约束，使用者负责判断。</p><p>把这三层责任分清楚，幻觉才会从一个模糊的模型缺陷，变成可以逐步治理的工程问题。</p></div></div>]]></description><link>https://binzi.top/posts/ai/%E6%80%8E%E4%B9%88%E5%87%8F%E5%B0%91%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%B9%BB%E8%A7%89</link><guid isPermaLink="true">https://binzi.top/posts/ai/%E6%80%8E%E4%B9%88%E5%87%8F%E5%B0%91%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%B9%BB%E8%A7%89</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 10 Apr 2026 06:41:35 GMT</pubDate></item><item><title><![CDATA[cookie、session、token]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/java/cookie%E3%80%81session%E3%80%81token">https://binzi.top/posts/java/cookie%E3%80%81session%E3%80%81token</a></blockquote><div><h1 id="cookiesessiontoken">cookie、session、token</h1><p>cookie<br/>服务端发给客户端，交给客户端保存的数据，每次请求，客户端带上这些数据，服务端就读取这些数据，鉴别身份。<br/>完全由客户端存储，和域名绑定，且不可跨域。<br/>数据大小一般有限制。<br/>容易被伪造，且客户端清除数据就丢失了</p><p>session<br/>服务端将客户端的信息保存在服务端，然后将一个sessionId传给客户端，客户端保存起来，下次请求携带sessionId，服务端就通过id找到客户端的身份数据。<br/>真正的数据存储在服务端，数据安全，且数据格式由服务端自定义。<br/>sessionId一般客户端通过cookie的形式存储起来，也有可能通过URL拼接来传递（防止浏览器不支持cookie）。</p><p>token<br/>将数据进行加密后，交给客户端存储，客户端每次请求携带这个数据，服务端解密，读出数据。<br/>最常见的实现就是JWT令牌<br/>客户端一般也是通过cookie存储<br/>支持跨域，只要数据能正确解密出来就没问题</p><p>总结<br/>可以看到，这三个东西并不是对立的，一般cookie作为底层技术支撑session和token的实现，而session和token的区别主要就在于，session是把数据存在服务端，而token是把数据加密起来交给客户端存储。</p></div></div>]]></description><link>https://binzi.top/posts/java/cookie%E3%80%81session%E3%80%81token</link><guid isPermaLink="true">https://binzi.top/posts/java/cookie%E3%80%81session%E3%80%81token</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 26 Jan 2026 07:54:15 GMT</pubDate></item><item><title><![CDATA[MySQL的锁]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL%E9%94%81">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL%E9%94%81</a></blockquote><div><h1 id="mysql">MySQL锁</h1><p><strong>全局锁</strong>：</p><p>flush tables with read lock，执行此条命令，整个数据库处于只读状态<br/>unlock tables， 释放全局锁，会话断开时，也会自动释放全局锁</p><p>一般用来<strong>全库逻辑备份</strong></p><p><strong>表级锁</strong>：MySQL 里面表级别的锁有这几种：</p><ul><li><ul><li>表锁：通过lock tables 语句可以对表加表锁，表锁除了会限制别的线程的读写外，也会限制本线程接下来的读写操作。</li><li>元数据锁：当我们对数据库表进行操作时，会自动给这个表加上 MDL，对一张表进行 CRUD 操作时，加的是 <strong>MDL 读锁</strong>；对一张表做结构变更操作的时候，加的是 <strong>MDL 写锁</strong>；MDL 是为了保证当用户对表执行 CRUD 操作时，防止其他线程对这个表结构做了变更。</li><li>意向锁：当执行插入、更新、删除操作，需要先对表加上「意向独占锁」，然后对该记录加独占锁。<strong>意向锁的目的是为了快速判断表里是否有记录被加锁</strong>。</li></ul></li></ul><p><strong>行级锁</strong>：InnoDB 引擎是支持行级锁的，而 MyISAM 引擎并不支持行级锁。</p><p><strong>记录锁(Record Lock)</strong></p><ul><li>锁住某行记录，分为读写锁，事务提交后自动释放，例如select * from table_name where id = 1 for update;就给id=1这行记录上了写锁</li></ul><p><strong>间隙锁(Gap Lock)</strong></p><ul><li>锁住行与行之间的间隙，防止其它事务在间隙中插入数据，产生幻读，间隙锁是不互斥的（如果涉及到记录锁或者临键锁，还是会互斥阻塞），两个事务可以拥有同一段间隙的间隙锁，但要插入数据时发现这段间隙被其它事务锁上了就插不了了。间隙锁是为了解决幻读，防止其它事务在区间内插入数据</li></ul><p><strong>临键锁(Next-Key Lock)</strong></p><ul><li>间隙锁 + 记录锁，加临键锁的时候是先加间隙锁，后加记录锁，这就有可能出现加间隙锁能成功，加记录锁被阻塞，然后间隙锁与其它事务发生死锁</li><li>前开后闭区间，锁住某行以及这一行之前的间隙，如(1, 3]，锁住3这一行和1到3行之间的间隙，因为有记录锁，会产生读写互斥，间隙锁之间不互斥</li></ul><p><strong>插入意向锁：</strong></p><ul><li>插入意向锁是一个间隙锁，但这个间隙锁实际上锁住的不是一个区间，而是区间中插入的那个点，所以算是一种特殊的间隙锁，插入意向锁和插入意向锁会互斥，即两个事务可以在同一个区间插入，但不能在同一个区间上相同的点插入；插入意向锁与间隙锁互斥，即插入的点所处的区间不能被其它事务的间隙锁锁住</li></ul><h2 id="">加锁算法</h2><blockquote><p><strong>抛开隔离级别谈加锁，都是耍流氓。</strong><br/>InnoDB 的加锁行为必须结合事务隔离级别来理解。</p></blockquote>
<h4 id="read-uncommitted">一、读未提交（Read Uncommitted）</h4><ul><li><strong>增（INSERT）</strong><br/>插入成功后，对新插入的记录加 <strong>记录锁</strong>，防止其他事务修改该记录。</li><li><strong>删（DELETE）</strong><br/>需要获取待删除记录的 <strong>记录锁</strong>，<br/>保证删除过程中其他事务无法使用该记录，且删除后不能再对其进行操作。</li><li><strong>改（UPDATE）</strong><br/>对需要修改的记录加 <strong>记录锁</strong>。</li><li><strong>查（SELECT）</strong>
<ul><li>普通 <code>SELECT</code>：不上锁，直接读取最新数据（包括未提交的数据）。</li><li><code>SELECT ... FOR UPDATE</code> / <code>SELECT ... LOCK IN SHARE MODE</code>：<br/>仅加 <strong>记录锁</strong>，<strong>不加间隙锁</strong>。</li></ul></li></ul><h4 id="read-committed">二、读已提交（Read Committed）</h4><ul><li><strong>增（INSERT）</strong><br/>插入后对新记录加 <strong>记录锁</strong>。</li><li><strong>删（DELETE）</strong><br/>对目标记录加 <strong>记录锁</strong>，保证删除过程的安全性。</li><li><strong>改（UPDATE）</strong><br/>对被修改的记录加 <strong>记录锁</strong>。</li><li><strong>查（SELECT）</strong>
<ul><li>普通 <code>SELECT</code>：不上锁，通过 <strong>MVCC</strong> 读取已提交的数据。</li><li><code>SELECT ... FOR UPDATE</code> / <code>SELECT ... LOCK IN SHARE MODE</code>：<br/>仅加 <strong>记录锁</strong>，<strong>不锁间隙</strong>。</li></ul></li></ul><h4 id="repeatable-readinnodb-">三、可重复读（Repeatable Read，InnoDB 默认）</h4><ul><li><strong>增（INSERT）</strong>
<ul><li>插入时使用 <strong>插入意向锁</strong>。</li><li>插入完成后，记录会被加上 <strong>记录锁（隐式锁，非立即可见）</strong>。</li></ul></li><li><strong>删（DELETE）</strong><br/>对删除范围加 <strong>临键锁（Next-Key Lock）</strong>，<br/>防止其他事务在该区间内插入新记录。</li><li><strong>改（UPDATE）</strong><br/>对目标记录加 <strong>临键锁</strong>。</li><li><strong>查（SELECT）</strong>
<ul><li>普通 <code>SELECT</code>：不上锁，通过 <strong>MVCC</strong> 读取快照数据。</li><li><code>SELECT ... FOR UPDATE</code> / <code>SELECT ... LOCK IN SHARE MODE</code>：<br/>加 <strong>临键锁</strong>，<strong>同时锁记录和间隙</strong>。</li></ul></li></ul><h4 id="serializable">四、串行化（Serializable）</h4><ul><li><strong>增（INSERT）</strong><br/>使用 <strong>插入意向锁</strong>，插入完成后记录被加上 <strong>记录锁</strong>。</li><li><strong>删（DELETE）</strong><br/>对删除区间加 <strong>临键锁</strong>，防止其他事务插入。</li><li><strong>改（UPDATE）</strong><br/>对修改范围加 <strong>临键锁</strong>。</li><li><strong>查（SELECT）</strong>
<ul><li>普通查询会 <strong>自动转化为</strong><br/><code>SELECT ... LOCK IN SHARE MODE</code>，<br/>对读取范围加 <strong>临键锁</strong>，锁定间隙。</li></ul></li></ul></div></div>]]></description><link>https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL%E9%94%81</link><guid isPermaLink="true">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL%E9%94%81</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 31 Dec 2025 17:10:00 GMT</pubDate></item><item><title><![CDATA[垃圾收集器（CMS-G1-ZGC）与垃圾回收算法]]></title><description><![CDATA[<link rel="preload" as="image" href="https://binzi.top/img/covers/1766223868563-25277577-8021-46f0-b6c0-eba574164d41.png"/><link rel="preload" as="image" href="https://binzi.top/img/covers/image-20251230150941338.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/java/%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8%E4%B8%8E%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95">https://binzi.top/posts/java/%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8%E4%B8%8E%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95</a></blockquote><div><h1 id="cms-g1-zgc">垃圾收集器（CMS-G1-ZGC）</h1><h3 id="cms-">CMS 垃圾收集器</h3><p><strong>JDK1.5时引入，JDK9被标记弃用，JDK14被移除</strong></p><p>CMS（Concurrent Mark Sweep，并发标记清除）收集器是以获取最短回收停顿时间为目标的收集器（追求低停顿），它在垃圾收集时使得用户线程和 GC 线程并发执行，因此在垃圾收集过程中用户也不会感到明显的卡顿。</p><ul><li>初始标记：Stop The World，仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。</li><li>并发标记：使用<strong>多条</strong>标记线程，与用户线程并发执行。此过程进行可达性分析，标记出所有废弃对象。速度很慢。</li><li>重新标记：Stop The World，使用多条标记线程并发执行，将刚才并发标记过程中新出现的废弃对象标记出来。</li><li>并发清除：只使用一条 GC 线程，与用户线程并发执行，清除刚才标记的对象。这个过程非常耗时。</li></ul><p>并发标记与并发清除过程耗时最长，且可以与用户线程一起工作，因此，<strong>总体上说</strong>，CMS 收集器的内存回收过程是与用户线程<strong>一起并发执行</strong>的。</p><p><img src="https://binzi.top/img/covers/1766223868563-25277577-8021-46f0-b6c0-eba574164d41.png" alt="img" height="0" width="0"/></p><p>CMS 的缺点：</p><ul><li>吞吐量低</li><li>无法处理浮动垃圾</li><li>使用“标记-清除”算法产生碎片空间，导致频繁 Full GC</li></ul><p>对于产生碎片空间的问题，可以通过开启 -XX:+UseCMSCompactAtFullCollection，在每次 Full GC 完成后都会进行一次内存压缩整理，将零散在各处的对象整理到一块。设置参数 -XX:CMSFullGCsBeforeCompaction 告诉 CMS，经过了 N 次 Full GC 之后再进行一次内存整理。</p><h3 id="g1-">G1 通用垃圾收集器</h3><p><strong>JDK7引入，JDK9取代CMS成为默认垃圾收集器，至少要4G内存才推荐使用G1</strong></p><p><img src="https://binzi.top/img/covers/image-20251230150941338.png" alt="image-20251230150941338"/></p><p>G1 是一款面向服务端应用的垃圾收集器，它没有新生代和老年代的概念，而是将堆划分为一块块独立的 Region。当要进行垃圾收集时，首先估计每个 Region 中垃圾的数量，每次都从垃圾回收价值最大的 Region 开始回收，因此可以获得最大的回收效率。</p><p>从整体上看， G1 是基于“<strong>标记-整理</strong>”算法实现的收集器，从局部（两个 Region 之间）上看是基于“复制”算法实现的，这意味着运行期间不会产生内存空间碎片。</p><p>这里抛个问题</p><p><code>一个对象和它内部所引用的对象可能不在同一个 Region 中，那么当垃圾回收时，是否需要扫描整个堆内存才能完整地进行一次可达性分析？</code></p><p>并不！每个 Region 都有一个记忆集（Remembered Set），用于记录本区域中所有对象引用的对象所在的区域，进行可达性分析时，只要在 GC Roots 中再加上记忆集即可防止对整个堆内存进行遍历。</p><p>如果不计算维护 Remembered Set 的操作，G1 收集器的工作过程分为以下几个步骤：</p><ul><li>初始标记：Stop The World，仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。</li><li>并发标记：使用<strong>一条</strong>标记线程与用户线程并发执行。此过程进行可达性分析，速度很慢。</li><li>最终标记：Stop The World，使用多条标记线程并发执行。</li><li>筛选回收：回收废弃对象，此时也要 Stop The World，并使用多条筛选回收线程并发执行。</li></ul><h3 id="zgc-">ZGC 收集器（低延迟垃圾收集器）</h3><p><strong>JDK11推出</strong><br/>与Shenandoah和G1一样，ZGC也采用基于Region 的堆内存布局，但与它们不同的是，ZGC的Region具有动态性——动态创建和销毁，以及动态的区域容量大小。</p><ul><li>小型Region：容量固定为 2MB，用于放置小于256KB的小对象</li><li>中型Region：容量固定为32MB，用于放置大于等于256KB但小于4MB的对象</li><li>大型Region：容量不固定，可以动态变化，但必须为2MB的整数倍，用于放置4MB或以上的大对象。每个大型</li></ul><p>Region中只会存放一个大对象，大型Region在ZGC的实现中是不会被重分配（重分配是ZGC的一种处理动作，用于复制对象的收集器阶段）的，因为复制一个大对象的代价非常高昂。<br/>ZGC收集器有一个标志性的设计是它采用的染色指针技术，染色指针是一种直接将少量额外的信息存储在指针上的技术<br/>Linux下64位指针的高18位不能用来寻址，但剩余的46位指针所能支持的64TB内存在今天仍然能够充分满足大型服务器的需要。鉴于此，ZGC的染色指针技术继续盯上了这剩下的46位指针宽度，将其高4位提取出来存储四个标志信息。通过这些标志位，虚拟机可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集（即被移动过）、是否只能通过finalize()方法才能被访问到。由于这些标志位进一步压缩了原本就只有46位的地址空间，也直接导致ZGC能够管理的内存不可以超过4TB（2的42次幂）</p><p>ZGC不通过原始快照或增量更新来实现并发标记，而是通过<strong>染色指针</strong></p><h1 id="">垃圾回收算法</h1><p><strong>标记-清除算法：</strong><br/>基础算法，后面两个算法基于此算法改进<br/>算法分为“标记”和“清除”两个阶段：首先标记出所有需要回收的对象，在标记完成后，统一回收掉所有被标记的对象，也可以反过来，标记存活的对象，统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。该算法两个缺点：</p><ol start="1"><li>执行效率不稳定</li><li>内存碎片化</li></ol><p><strong>标记-复制算法：</strong><br/>适用于新生代的算法，将可用内存按容量划分为大小相等的两块，每次只使用其中的一块。当这一块的内存用完了，就将还存活着的对象复制到另外一块上面，然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的，这种算法将会产生大量的内存间复制的开销，但对于多数对象都是可回收的情况，算法需要复制的就是占少数的存活对象，而且每次都是针对整个半区进行内存回收，分配内存时也就不用考虑有空间碎片的复杂情况，只要移动堆顶指针，按顺序分配即可。算法缺点很明显：少了一半空间</p><p>新生代中的对象有98%熬不过第一轮收集。因此并不需要按照1∶1的比例来划分新生代的内存空间。</p><p><strong>标记-整理算法：</strong><br/>针对老年代对象的存亡特征，1974年Edward Lueders提出了另外一种有针对性的 “标记-整理”（Mark-Compact）算法，其中的标记过程仍然与“标记-清除”算法一样，但后续步骤不是直接对可回收对象进行清理，而是让所有存活的对象都向内存空间一端移动，然后直接清理掉边界以外的内存</p><p><strong>分代收集算法</strong>：<br/>根据对象存活时间将堆划分为新生代和老年代，新生代使用复制算法，老年代使用标记-整理或标记-清除算法。</p><p><strong>三色标记法：</strong><br/>可达性分析算法中，标记过程需要stop the world，保证全局获得一致性快照，这个操作可否与用户线程并发？<br/>先来看三色标记法：把遍历对象图过程中遇到的对象，按照“是否访问过”这个条件标记成以下三种颜色：</p><ul><li>白色：表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段，所有的对象都是白色的，若在分析结束的阶段，仍然是白色的对象，即代表不可达，需要被回收</li><li>黑色：表示对象已经被垃圾收集器访问过，且所有引用了这个对象的对象都已经扫描过。黑色的对象代表已经扫描过，它是安全存活的，不用被回收，如果有其他对象引用指向了黑色对象，无须重新扫描一遍。黑色对象不可能直接（不经过灰色对象）指向某个白色对象。</li><li>灰色：表示对象已经被垃圾收集器访问过，但这个对象上至少存在一个引用还没有被扫描过。是个中间态，当扫描完成后，不会出现这个颜色，整个图非黑即白</li></ul><p>可达性分析算法其实就是从GC Roots出发，将图（对象直接的引用关系图）波浪式地由白色转为黑色，其中灰色是黑白之间的过渡色。<br/>这个标记法在用户线程和收集器并发工作下可能存在问题：</p><p>是把原本消亡的对象错误标记为存活，这不是好事，但其实是可以容忍的，只不过产生了一点逃过本次收集的浮动垃圾而已，下次收集清理掉就好。</p><p>把原本存活的对象错误标记为已消亡，这就是非常致命的后果了，程序肯定会因此发生错误</p><h3 id="--">为什么标记-复制适用于新生代，标记-整理适用于老年代？</h3><p>新生代需要被清理的对象多，复制只需要复制少量存活对象<br/>老年代存活的对象多，不能使用Eden Survivor那套，不然内存就不够了，当然标记-整理也是一项很负重的操作，但如果不整理，就需要额外使用页表等方式标记哪些空间可用来解决空间碎片化问题，这也会导致额外负担，所以从整个程序的吞吐量考虑，标记-整理是较好的选择<br/>当然老年代也可以先标记-清除，等内存空间碎片化到一定程度时，进行一次标记整理</p></div></div>]]></description><link>https://binzi.top/posts/java/%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8%E4%B8%8E%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95</link><guid isPermaLink="true">https://binzi.top/posts/java/%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8%E4%B8%8E%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 30 Dec 2025 15:13:00 GMT</pubDate></item><item><title><![CDATA[类加载机制与类加载器]]></title><description><![CDATA[<link rel="preload" as="image" href="https://binzi.top/img/covers/image-20251228153154941.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/java/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6%E4%B8%8E%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8">https://binzi.top/posts/java/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6%E4%B8%8E%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8</a></blockquote><div><h1 id="">类加载机制/类加载器</h1><h4 id="">类加载的完整流程（生命周期）</h4><p><img src="https://binzi.top/img/covers/image-20251228153154941.png" alt="image-20251228153154941" height="0" width="0"/></p><p>类从加载到卸载经历 5 个阶段，其中<strong>加载、验证、准备、初始化、卸载</strong>的顺序是固定的，<strong>解析</strong>阶段可能与初始化交叉进行：</p><h5 id="1-loading">1. 加载（Loading）</h5><ul><li><strong>任务</strong>：通过类的全限定名（如<code>com.example.User</code>）获取其字节码数据，并生成<code>Class</code>对象。</li><li><strong>来源</strong>：字节码可来自本地文件、网络、数据库、动态生成（如 CGLib）等。</li><li><strong>关键组件</strong>：类加载器（ClassLoader）负责此阶段。</li></ul><h5 id="2-verification">2. 验证（Verification）</h5><ul><li><strong>任务</strong>：确保字节码符合 JVM 规范，避免恶意或无效字节码危害虚拟机安全。</li><li><p>验证内容</p><ul><li>文件格式验证（魔数、版本号等）</li><li>元数据验证（类继承关系、字段方法合法性）</li><li>字节码验证（指令逻辑正确性）</li><li>符号引用验证（常量池引用有效性）</li></ul></li></ul><h5 id="3-preparation">3. 准备（Preparation）</h5><ul><li><p>任务</p><p>：为类的静态变量分配内存并设置</p><p>初始默认值</p><p>（非显式赋值）。</p><ul><li>例如：<code>public static int a = 10;</code>在准备阶段<code>a</code>的值为<code>0</code>（int 默认值），显式赋值在初始化阶段执行。</li></ul></li><li><p><strong>注意</strong>：仅静态变量（类变量）参与此阶段，实例变量在对象创建时分配内存。</p></li></ul><h5 id="4-resolution">4. 解析（Resolution）</h5><ul><li><strong>任务</strong>：将常量池中的<strong>符号引用</strong>（如类名、方法名）转换为<strong>直接引用</strong>（内存地址）。</li><li><strong>触发时机</strong>：通常在初始化前完成，但某些情况下（如动态绑定）会延迟到运行时解析。</li></ul><h5 id="5-initialization-clinit">5. 初始化（Initialization）执行 <code>&lt;clinit&gt;</code>，真正赋值</h5><ul><li><p>任务</p><p>：执行类的初始化逻辑，包括：</p><ul><li>静态变量的显式赋值</li><li>静态代码块（<code>static {}</code>）的执行</li></ul></li><li><p>顺序规则</p><p>：</p><ul><li>父类初始化优先于子类</li><li>静态变量和静态代码块按代码出现顺序执行</li></ul></li><li><p>触发条件</p><p>（主动使用）：</p><ul><li>创建实例、调用静态方法 / 变量</li><li>反射（<code>Class.forName()</code>）</li><li>启动类（含<code>main</code>方法的类）</li><li>初始化子类时触发父类初始化</li></ul></li></ul><h2 id="">什么时候加载类？</h2><p>JVM规范并没有强制约束，交给虚拟机具体实现来自由把握</p><h2 id="">类加载器</h2><p>类加载器负责 “加载” 阶段，通过全限定名获取字节码。JVM 中存在以下几类加载器：</p><h5 id="1-">1. 内置类加载器</h5><ul><li><p><strong>启动类加载器（Bootstrap ClassLoader）</strong></p><ul><li>由 C++ 实现（非 Java 类），负责加载 JDK 核心类库（如<code>rt.jar</code>、<code>resources.jar</code>）。</li><li><p><strong>最顶层</strong>负责加载：</p><p><code>java.lang.*</code></p><p><code>java.util.*</code></p><p>加载路径：<code>JAVA_HOME/lib</code><strong>由 C/C++ 实现</strong></p></li><li><p>父加载器为<code>null</code>，不继承<code>java.lang.ClassLoader</code>。</p></li></ul></li><li><p><strong>扩展类加载器（Extension ClassLoader）</strong></p><ul><li>加载 JDK 扩展目录（如<code>jre/lib/ext</code>）中的类。</li><li>父加载器为启动类加载器。</li></ul></li><li><p><strong>应用程序类加载器（Application ClassLoader）</strong></p><ul><li>加载用户类路径（<code>classpath</code>）下的类，是默认的类加载器。</li><li>父加载器为扩展类加载器。</li></ul></li></ul><h5 id="2-">2. 自定义类加载器</h5><ul><li>继承<code>java.lang.ClassLoader</code>，重写<code>findClass()</code>方法（推荐）或<code>loadClass()</code>方法。</li><li>应用场景：热部署、加密类解密、从非标准来源加载类等。</li></ul><h5 id="3-">3. 双亲委派模型</h5><ul><li><strong>定义</strong>：类加载时，先委托父加载器加载，父加载器无法加载时才由当前加载器尝试加载。</li><li><strong>流程</strong>：<br/>自定义加载器 → 应用程序类加载器 → 扩展类加载器 → 启动类加载器<br/>（若所有父加载器均无法加载，则由当前加载器调用<code>findClass()</code>加载）</li><li><strong>作用</strong>：
<ul><li>避免类重复加载（保证同一个类在 JVM 中唯一）。</li><li>保护核心类库（如<code>java.lang.String</code>不会被恶意替换）。</li></ul></li><li><strong>破坏双亲委派</strong>：<br/>可通过重写<code>loadClass()</code>方法实现（如 Tomcat 的类加载器需隔离不同 Web 应用）。</li></ul><h2 id="">类的卸载</h2><ul><li>当类的<code>Class</code>对象不再被引用，且加载该类的类加载器被回收时，类会被卸载。</li><li>条件：
<ul><li>该类的所有实例已被回收。</li><li>该类的<code>Class</code>对象无引用。</li><li>加载该类的类加载器已被回收。</li></ul></li><li>注意：JVM 内置加载器加载的类（如核心类库）通常不会被卸载，缘于其加载器始终存在。</li></ul><h2 id="">关键特性与问题</h2><ol start="1"><li><strong>类的唯一性</strong><br/><strong>一个类的唯一性由 “全限定名 + 类加载器” 共同决定。不同加载器加载的同全限定名类视为不同类。</strong></li><li><strong>延迟加载与预加载</strong>
<ul><li>大多数类采用延迟加载（首次使用时加载）。</li><li>少数类（如<code>java.lang.Object</code>）会被 JVM 预加载。</li></ul></li><li><strong>常见异常</strong>
<ul><li><code>ClassNotFoundException</code>：类加载器无法找到指定类（如类路径错误）。</li><li><code>NoClassDefFoundError</code>：类编译时存在，但运行时未找到（如依赖缺失）。</li></ul></li></ol><h2 id="">破坏双亲委派模型：</h2><p>自定义加载器的话，需要继承 ClassLoader 。如果我们不想打破双亲委派模型，就重写 ClassLoader 类中的 findClass() 方法即可，无法被父类加载器加载的类最终会通过这个方法被加载。但是，如果想打破双亲委派模型<strong>则需要重写 loadClass() 方法。</strong></p></div></div>]]></description><link>https://binzi.top/posts/java/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6%E4%B8%8E%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8</link><guid isPermaLink="true">https://binzi.top/posts/java/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6%E4%B8%8E%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sun, 28 Dec 2025 11:13:00 GMT</pubDate></item><item><title><![CDATA[MySQL的MVCC]]></title><description><![CDATA[<link rel="preload" as="image" href="https://binzi.top/img/covers/image-20251116012419555.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL%E7%9A%84MVCC">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL%E7%9A%84MVCC</a></blockquote><div><h1 id="mvcc">MVCC多版本并发控制</h1><p><strong>MVCC 的目标：让读操作尽量不阻塞写操作，写操作也尽量不阻塞读操作。</strong></p><h5 id="">核心机制：</h5><h4 id="mvcc">MVCC主要依赖以下机制实现：</h4><p><strong>数据库行记录中的隐藏字段</strong>：最近修改本行数据的事务id、回滚指针指向本行数据的上个版本（其实是指向undo，通过undo计算上一个版本）</p><p>•<strong>undo log</strong>：回滚日志，在insert、delete、update时产生，记载了与操作相反的操作，比如delete操作对应了一条insert日志</p><p>•<strong>readview</strong>：快照读SQL执行时产生的读视图，生成的一个快照，记录了以下字段，用于数据的可见性判断：◦</p><ol start="1"><li>当前活跃的事务id集合◦</li><li>最小活跃事务id◦</li><li>预分配事务id，即应该分配的下一个事务id◦</li><li>readview创建者id，即本事务id</li></ol><h4 id="mvcc">MVCC如何判断数据的可见性？</h4><p>首先根据本条数据的是否是由本事务生成的，如果是，那可见，毕竟自己做的修改自己肯定是能看见的<br/>再判断本条数据的事务id是否比最小活跃事务id要小，如果是，那可见，因为这说明这条数据是已经被其它事务提交的，本事务应该可见<br/>再判断本条数据的事务id是否不在活跃事务id集合中，如果不在，那可见，因为这说明也是其它事务提交了的数据</p><p>如果本条数据对本事务不可见，就根据隐藏字段 + undo log计算出上一个版本的数据，然后继续判断，直到找到某条对本事务可见的数据</p><h3 id="rc--rr--mvcc-">RC 和 RR 隔离级别下 MVCC 的差异</h3><p>在事务隔离级别 <code>RC</code> 和 <code>RR</code> （InnoDB 存储引擎的默认事务隔离级别）下，<code>InnoDB</code> 存储引擎使用 <code>MVCC</code>（非锁定一致性读），但它们生成 <code>Read View</code> 的时机却不同</p><ul><li>在 RC 隔离级别下的 <strong><code>每次select</code></strong> 查询前都生成一个<code>Read View</code> (m_ids 列表)</li><li>在 RR 隔离级别下只在事务开始后 <strong><code>第一次select</code></strong> 数据前生成一个<code>Read View</code>（m_ids 列表）</li></ul><h2 id="mvccnext-key-lock-">MVCC➕Next-key-Lock 防止幻读</h2><p><img src="https://binzi.top/img/covers/image-20251116012419555.png" alt="image-20251116012419555" height="0" width="0"/></p><p><code>InnoDB</code>存储引擎在 RR 级别下通过 <code>MVCC</code>和 <code>Next-key Lock</code> 来解决幻读问题：</p><p><strong>1、执行普通 <code>select</code>，此时会以 <code>MVCC</code> 快照读的方式读取数据</strong></p><p>在快照读的情况下，RR 隔离级别只会在事务开启后的第一次查询生成 <code>Read View</code> ，并使用至事务提交。所以在生成 <code>Read View</code> 之后其它事务所做的更新、插入记录版本对当前事务并不可见，实现了可重复读和防止快照读下的 “幻读”</p><p><strong>2、执行 select…for update/lock in share mode、insert、update、delete 等当前读</strong></p><p>在当前读下，读取的都是最新的数据，如果其它事务有插入新的记录，并且刚好在当前事务查询范围内，就会产生幻读！<code>InnoDB</code> 使用 <a href="https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-next-key-locks">Next-key Lock</a> 来防止这种情况。当执行当前读时，会锁定读取到的记录的同时，锁定它们的间隙，防止其它事务在查询范围内插入数据。只要我不让你插入，就不会发生幻读</p></div></div>]]></description><link>https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL%E7%9A%84MVCC</link><guid isPermaLink="true">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL%E7%9A%84MVCC</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 26 Dec 2025 17:10:00 GMT</pubDate></item><item><title><![CDATA[SpringMVC执行流程]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/java/SpringMVC%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B">https://binzi.top/posts/java/SpringMVC%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B</a></blockquote><div><h1 id="springmvc">SpringMVC执行流程</h1><p>请求先到 <strong>DispatcherServlet（前端控制器）</strong>，它负责调度 <strong>HandlerMapping → HandlerAdapter → Controller → ViewResolver → View</strong>。</p><p>Spring MVC 采用前端控制器模式，所有请求先进入 DispatcherServlet。DispatcherServlet 通过 HandlerMapping 找到对应的 Controller，再通过 HandlerAdapter 调用方法执行。Controller 返回结果后，如果是视图名则交给 ViewResolver 解析并渲染视图；如果是 @ResponseBody，则通过 HttpMessageConverter 转换为 JSON 返回给客户端。</p><h3 id="springmvc">SpringMVC是什么?</h3><p>Spring MVC 是 Spring 提供的一个基于 MVC 设计模式的 Web 框架，通过 DispatcherServlet 作为前端控制器，结合 HandlerMapping、HandlerAdapter 等组件完成请求的分发和处理，支持注解方式开发，能够方便地实现请求参数绑定和响应结果返回。</p></div></div>]]></description><link>https://binzi.top/posts/java/SpringMVC%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B</link><guid isPermaLink="true">https://binzi.top/posts/java/SpringMVC%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Thu, 25 Dec 2025 13:05:00 GMT</pubDate></item><item><title><![CDATA[MySqL事务特性，隔离级别]]></title><description><![CDATA[<link rel="preload" as="image" href="https://binzi.top/img/covers/image-20251224220314987.png"/><link rel="preload" as="image" href="https://binzi.top/img/covers/image-20251224221935836.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySqL%E4%BA%8B%E5%8A%A1%E7%89%B9%E6%80%A7%EF%BC%8C%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySqL%E4%BA%8B%E5%8A%A1%E7%89%B9%E6%80%A7%EF%BC%8C%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB</a></blockquote><div><h3 id="mysql"><strong>MySqL事务隔离级别</strong></h3><p>事务的特性：</p><p>原子性：<strong>事务要么全部提交成功要么全部失败</strong></p><p>一致性：<strong>事务操作前和操作后具有完整性约束，数据库保持一致性状态</strong></p><p>隔离性：<strong>事务执行时相互隔离互不影响，可以防止多个事务并发执行由于交叉执行导致的不一致</strong></p><p>持久性：<strong>事务操作结束后修改是永久性的，即使关机故障也不会消失</strong></p><h3 id="mysql-innodb-"><strong>MySQL InnoDB 引擎通过什么技术来保证事务的这四个特性的呢？</strong></h3><ul><li>持久性是通过 redo log （重做日志）来保证的；</li><li>原子性是通过 undo log（回滚日志） 来保证的；</li><li>隔离性是通过 MVCC（多版本并发控制） 或锁机制来保证的；</li><li>一致性则是通过持久性+原子性+隔离性来保证；</li></ul><h3 id="">事务的隔离级别有哪些？</h3><h5 id=""><strong>读未提交</strong></h5><p>​最低的隔离级别， 会导致脏读、幻读或不可重复读。</p><h5 id=""><strong>读已提交</strong></h5><p>​允许读取并发事务已经提交的数据，可以阻止脏读，但是幻读或不可重复读仍有可能发生。这是大多数数据库（如 Oracle, SQL Server）的默认隔离级别。</p><h5 id=""><strong>可重复读</strong></h5><p>​对同一字段的多次读取结果都是一致的，除非数据是被本身事务自己所修改，可以阻止脏读和不可重复读，但幻读仍有可能发生。并且，InnoDB 在此级别下通过 MVCC（多版本并发控制） 和 Next-Key Locks（间隙锁+行锁） 机制，在很大程度上解决了幻读问题。</p><p><strong>RR隔离级别甚至连部分情况的可重复读也没解决：事务A先快照读查到数据a=10，事务B更新a=15并提交，事务A再当前读数据，读出来是a=15，两次前后读取不一样。</strong></p><p>当前读一定读取“最新已提交版本（Latest Committed Version）”，而不是事务开始时的快照。</p><p><strong>可串行化</strong></p><p>​最高的隔离级别，完全服从 ACID 的隔离级别。所有的事务依次逐个执行，这样事务之间就完全不可能产生干扰，也就是说，该级别可以防止脏读、不可重复读以及幻读。</p><h3 id="">事务执行可能出现的问题</h3><p>脏读 ：<strong>当一个事务读到另一个没有提交事务修改过的数据，此时就发生脏读了。通过读已提交可以避免</strong></p><p>不可重复读 ： <strong>在一个事务内多次读取同一个数据，如果出现前后两次读到的数据不一样的情况，就意味着发生了「不可重复读」现象。</strong></p><p>幻读 : <strong>在一个事务内多次查询某个符合查询条件的「记录数量」，如果出现前后两次查询到的记录数量不一样的情况，就意味着发生了「幻读」现象。</strong></p><p><img src="https://binzi.top/img/covers/image-20251224220314987.png" alt="image-20251224220314987" height="0" width="0"/></p><p><img src="https://binzi.top/img/covers/image-20251224221935836.png" alt="image-20251224221935836"/></p><p>RR 隔离级别下如果第一次是快照读、第二次是当前读，确实可能读到不同的值；但严格来说，这并不属于不可重复读，因为比较的是两种不同的读语义。不可重复读只讨论同一种读方式。<br/>在 RC 隔离级别下，虽然普通快照读无法避免不可重复读，但如果显式使用当前读并加锁，同样可以通过锁机制避免前后读取不一致。</p></div></div>]]></description><link>https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySqL%E4%BA%8B%E5%8A%A1%E7%89%B9%E6%80%A7%EF%BC%8C%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB</link><guid isPermaLink="true">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySqL%E4%BA%8B%E5%8A%A1%E7%89%B9%E6%80%A7%EF%BC%8C%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 24 Dec 2025 14:34:00 GMT</pubDate></item><item><title><![CDATA[Redis的ziplist到ListPack]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E7%9A%84ziplist%E5%88%B0ListPack">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E7%9A%84ziplist%E5%88%B0ListPack</a></blockquote><div><h2 id="redisziplistlistpack">Redis的压缩列表ziplist到紧凑列表listpack</h2><h3 id="ziplist-">ziplist 的结构（老版本用的）</h3><p>大概长这样：</p><pre class=""><code class="">[zlbytes][zltail][zllen][entry1][entry2]...[entryN][0xFF]
● *zlbytes*，记录整个压缩列表占用对内存字节数；
● *zltail*，记录压缩列表「尾部」节点距离起始地址由多少字节，也就是列表尾的偏移量；
● *zllen*，记录压缩列表包含的节点数量；
● *0xFF*，标记压缩列表的结束点，固定值 0xFF（十进制255）。
</code></pre>
<p>每个 <code>entry</code> 里：</p><pre class=""><code class="">[prevlen][encoding][data]
</code></pre>
<ul><li><code>prevlen</code>：前一个 entry 的长度（1 或 5 字节）</li><li><code>encoding</code>：当前 entry 是字符串还是整数，长度是多少</li><li><code>data</code>：真正的数据</li></ul><h3 id="listpack-">listpack 的结构（新版本用的）</h3><p>大概长这样：</p><pre class=""><code class="">[total_bytes][count][entry1][entry2]...[entryN][0xFF]
total_bytes：整个 listpack 的总字节数
count：entry 的个数（元素数量）
● *0xFF*，标记压缩列表的结束点，固定值 0xFF（十进制255）。
</code></pre>
<p>每个 <code>entry</code> 里：</p><pre class=""><code class="">[encoding+len][data][backlen]
[encoding+len]：
1.数据类型（string/int…）
2.字符串长度（完整长度信息）
3.如果长度太长，encoding 会表示“后面几个字节继续存长度”
backlen ：当前整个 entry 的总字节数
</code></pre>
<p>注意两点变化：</p><ol start="1"><li><strong>没有 prevlen 了</strong>（不再依赖“记录前一个元素长度”来反向遍历）</li><li>多了 <strong>backlen</strong>（记录自己的完整长度，用来从后往前遍历）</li></ol><p>这是 ziplist 被吐槽最多的一个点：<strong>级联更新（cascading update）</strong>。</p><h2 id="1ziplist-">1.ziplist 的问题</h2><ul><li>每个 entry 前面有个 <code>prevlen</code> 字段，记录 <em>前一个 entry 的长度</em></li><li><code>prevlen</code> 自身可能是 <strong>1 字节</strong> 或 <strong>5 字节</strong>
<ul><li>如果前一个 entry 变大了，从 253 → 300 字节</li><li>起初 <code>prevlen</code> = 1 字节，现在需要 5 字节</li></ul></li><li>那么：
<ul><li>当前 entry 要扩展 4 字节</li><li>它变长后，下一个 entry 的 <code>prevlen</code> 又可能从 1 → 5</li><li>如此继续……导致<strong>后面一串 entry 都要重写、搬移内存</strong></li></ul></li></ul><p>结果就是：<strong>某个元素变长，可能触发 O(N) 的连锁内存搬移</strong>，列表越大越惨，而且实现特别绕，也容易出 bug。</p><h2 id="2listpack-">2.listpack 如何改进？</h2><ul><li>listpack 不再在每个元素开头放 <code>prevlen</code></li><li>改为在 <strong>元素末尾</strong> 放一个 <code>backlen</code>，记录<strong>这个元素自己的长度</strong></li><li>反向遍历时：
<ul><li>从尾开始，先读 backlen → 退回去这么多字节 → 得到前一个元素起始位置</li></ul></li></ul><p>这样改的一个好处是：</p><ul><li>你修改某个元素的长度时，只需要考虑它<strong>自己的 backlen</strong></li><li>不会像 ziplist 那样，影响后面每一个元素的 <code>prevlen</code> 字段</li></ul><p>虽然单个元素变长还是需要移动后面的内存，但<strong>不会再产生那种“级联字段变化”的连环改动</strong>，整体逻辑简单且更可控。</p><hr/><h2 id="3-">3. 存储编码更紧凑，节省内存</h2><p>listpack 是在 ziplist 的经验基础上重新设计的，目标就是：</p><blockquote><p><strong>“既要省内存，又要减少复杂边界情况”</strong></p></blockquote>
<p>主要变化：</p><ol start="1"><li><strong>编码种类更合理</strong>
<ul><li>对整数、短字符串等使用多种变长编码方案</li><li>比 ziplist 更灵活、更紧凑</li></ul></li><li><strong>去掉多余的头部字段</strong>
<ul><li>ziplist 有 <code>zltail</code> 等字段</li><li>listpack 头部只保留对操作真正有用的字段（总字节数、元素数），结构更轻量</li></ul></li><li><strong>统一整数/字符串编码格式</strong>
<ul><li>在不同模块（hash、zset、list）里都可以复用 listpack</li><li>更利于代码维护和优化</li></ul></li></ol><p>总体效果：<strong>在很多实际场景中，listpack 比 ziplist 更省内存，尤其是大量小元素时</strong>。</p><hr/><h2 id="4-">4. 性能与安全性方面的改进</h2><h3 id="-">① 性能</h3><ul><li>减少了级联更新的问题，尤其是<strong>修改中间元素时</strong>更稳定</li><li>更简单的编码与解析逻辑，有利于编译器优化、CPU cache 命中</li></ul><h3 id="-">② 安全性</h3><p>ziplist 曾经出过多次 <strong>整数溢出 / 越界</strong> 的安全漏洞（CVE），原因之一就是：</p><ul><li>设计偏复杂，各种长度字段 + 级联更新</li><li>很难做到所有边界都不出错</li></ul><p>listpack 从设计上就尽量：</p><ul><li>简化字段</li><li>减少依赖“前一个元素长度”的逻辑</li><li>更好地检查长度合法性</li></ul></div></div>]]></description><link>https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E7%9A%84ziplist%E5%88%B0ListPack</link><guid isPermaLink="true">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E7%9A%84ziplist%E5%88%B0ListPack</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sun, 21 Dec 2025 14:24:00 GMT</pubDate></item><item><title><![CDATA[HotSpot 垃圾收集器]]></title><description><![CDATA[<link rel="preload" as="image" href="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/jvm@main/images/serial.png"/><link rel="preload" as="image" href="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/jvm@main/images/parnew.png"/><link rel="preload" as="image" href="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/jvm@main/images/cms.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/java/HotSpot%20%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8">https://binzi.top/posts/java/HotSpot%20%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8</a></blockquote><div><h1 id="hotspot-">HotSpot 垃圾收集器</h1><p>HotSpot 虚拟机提供了多种垃圾收集器，每种收集器都有各自的特点，虽然我们要对各个收集器进行比较，但并非为了挑选出一个最好的收集器。我们选择的只是对具体应用最合适的收集器。</p><h2 id="">新生代垃圾收集器</h2><h3 id="serial-">Serial 垃圾收集器（单线程）</h3><p>只开启<strong>一条</strong> GC 线程进行垃圾回收，并且在垃圾收集过程中停止一切用户线程，即 Stop The World。</p><p>一般客户端应用所需内存较小，不会创建太多对象，而且堆内存不大，因此垃圾收集器回收时间短，即使在这段时间停止一切用户线程，也不会感觉明显卡顿。因此 Serial 垃圾收集器<strong>适合客户端</strong>使用。</p><p>由于 Serial 收集器只使用一条 GC 线程，避免了线程切换的开销，从而简单高效。</p><p><img src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/jvm@main/images/serial.png" alt="Serial" height="0" width="0"/></p><h3 id="parnew-">ParNew 垃圾收集器（多线程）</h3><p>ParNew 是 Serial 的多线程版本。由多条 GC 线程并行地进行垃圾清理。但清理过程依然需要 Stop The World。</p><p>ParNew 追求“<strong>低停顿时间</strong>”,与 Serial 唯一区别就是使用了多线程进行垃圾收集，在多 CPU 环境下性能比 Serial 会有一定程度的提升；但<strong>线程切换需要额外的开销</strong>，因此在单 CPU 环境中表现不如 Serial。</p><p><img src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/jvm@main/images/parnew.png" alt="ParNew"/></p><h3 id="parallel-scavenge-">Parallel Scavenge 垃圾收集器（多线程）</h3><p>Parallel Scavenge 和 ParNew 一样，都是多线程、新生代垃圾收集器。但是两者有巨大的不同点：</p><ul><li>Parallel Scavenge：追求 CPU 吞吐量，能够在较短时间内完成指定任务，因此适合没有交互的后台计算。</li><li>ParNew：追求降低用户停顿时间，适合交互式应用。</li></ul><pre class=""><code class="">吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
</code></pre>
<p>追求高吞吐量，可以通过减少 GC 执行实际工作的时间，然而，仅仅偶尔运行 GC 意味着每当 GC 运行时将有许多工作要做，因为在此期间积累在堆中的对象数量很高。单个 GC 需要花更多的时间来完成，从而导致更高的暂停时间。而考虑到低暂停时间，最好频繁运行 GC 以便更快速完成，反过来又导致吞吐量下降。</p><ul><li>通过参数 -XX:GCTimeRadio 设置垃圾回收时间占总 CPU 时间的百分比。</li><li>通过参数 -XX:MaxGCPauseMillis 设置垃圾处理过程最久停顿时间。</li><li>通过命令 -XX:+UseAdaptiveSizePolicy 开启自适应策略。我们只要设置好堆的大小和 MaxGCPauseMillis 或 GCTimeRadio，收集器会自动调整新生代的大小、Eden 和 Survivor 的比例、对象进入老年代的年龄，以最大程度上接近我们设置的 MaxGCPauseMillis 或 GCTimeRadio。</li></ul><h2 id="">老年代垃圾收集器</h2><h3 id="serial-old-">Serial Old 垃圾收集器（单线程）</h3><p>Serial Old 收集器是 Serial 的老年代版本，都是单线程收集器，只启用一条 GC 线程，都适合客户端应用。它们唯一的区别就是：Serial Old 工作在老年代，使用“标记-整理”算法；Serial 工作在新生代，使用“复制”算法。</p><h3 id="parallel-old-">Parallel Old 垃圾收集器（多线程）</h3><p>Parallel Old 收集器是 Parallel Scavenge 的老年代版本，追求 CPU 吞吐量。</p><h3 id="cms-">CMS 垃圾收集器</h3><p>CMS（Concurrent Mark Sweep，并发标记清除）收集器是以获取最短回收停顿时间为目标的收集器（追求低停顿），它在垃圾收集时使得用户线程和 GC 线程并发执行，因此在垃圾收集过程中用户也不会感到明显的卡顿。</p><ul><li>初始标记：Stop The World，仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。</li><li>并发标记：使用<strong>多条</strong>标记线程，与用户线程并发执行。此过程进行可达性分析，标记出所有废弃对象。速度很慢。</li><li>重新标记：Stop The World，使用多条标记线程并发执行，将刚才并发标记过程中新出现的废弃对象标记出来。</li><li>并发清除：只使用一条 GC 线程，与用户线程并发执行，清除刚才标记的对象。这个过程非常耗时。</li></ul><p>并发标记与并发清除过程耗时最长，且可以与用户线程一起工作，因此，<strong>总体上说</strong>，CMS 收集器的内存回收过程是与用户线程<strong>一起并发执行</strong>的。</p><p><img src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/jvm@main/images/cms.png" alt="CMS"/></p><p>CMS 的缺点：</p><ul><li>吞吐量低</li><li>无法处理浮动垃圾</li><li>使用“标记-清除”算法产生碎片空间，导致频繁 Full GC</li></ul><p>对于产生碎片空间的问题，可以通过开启 -XX:+UseCMSCompactAtFullCollection，在每次 Full GC 完成后都会进行一次内存压缩整理，将零散在各处的对象整理到一块。设置参数 -XX:CMSFullGCsBeforeCompaction 告诉 CMS，经过了 N 次 Full GC 之后再进行一次内存整理。</p><h2 id="g1-">G1 通用垃圾收集器</h2><p>G1 是一款面向服务端应用的垃圾收集器，它没有新生代和老年代的概念，而是将堆划分为一块块独立的 Region。当要进行垃圾收集时，首先估计每个 Region 中垃圾的数量，每次都从垃圾回收价值最大的 Region 开始回收，因此可以获得最大的回收效率。</p><p>从整体上看， G1 是基于“标记-整理”算法实现的收集器，从局部（两个 Region 之间）上看是基于“复制”算法实现的，这意味着运行期间不会产生内存空间碎片。</p><p>这里抛个问题</p><blockquote><p>一个对象和它内部所引用的对象可能不在同一个 Region 中，那么当垃圾回收时，是否需要扫描整个堆内存才能完整地进行一次可达性分析？</p></blockquote>
<p>并不！每个 Region 都有一个 Remembered Set，用于记录本区域中所有对象引用的对象所在的区域，进行可达性分析时，只要在 GC Roots 中再加上 Remembered Set 即可防止对整个堆内存进行遍历。</p><p>如果不计算维护 Remembered Set 的操作，G1 收集器的工作过程分为以下几个步骤：</p><ul><li>初始标记：Stop The World，仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。</li><li>并发标记：使用<strong>一条</strong>标记线程与用户线程并发执行。此过程进行可达性分析，速度很慢。</li><li>最终标记：Stop The World，使用多条标记线程并发执行。</li><li>筛选回收：回收废弃对象，此时也要 Stop The World，并使用多条筛选回收线程并发执行。</li></ul></div></div>]]></description><link>https://binzi.top/posts/java/HotSpot%20%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8</link><guid isPermaLink="true">https://binzi.top/posts/java/HotSpot%20%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 19 Dec 2025 12:43:00 GMT</pubDate></item><item><title><![CDATA[Redis网络模型]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B</a></blockquote><div><h1 id="redisio">Redis网络模型（io多路复用）</h1><p><strong>阻塞IO(Blocking IO)</strong></p><p>假设服务端只开启一个线程处理请求，第一个请求到来，开始调用内核read函数，然后就会发生阻塞，第二个请求到来时服务端将无法处理，只能等第一个请求读取完成。这种方式的缺点很明显，每次只能处理一个请求，无法发挥cpu多核优势，性能低下。</p><p>为了解决这个问题，我们可以引入多线程，这样就可以同时处理多个请求了，但服务端可能同时有成千上万的请求需要处理，随之而来的是线程数膨胀，频繁创建、销毁线程带来的性能影响，当然我们可以使用线程池，但服务能处理的总体数量就会受限于线程池线程数量。</p><p><strong>非阻塞IO(NON-Blocking IO)</strong></p><p>相比阻塞IO，非阻塞IO会立即返回，调用者不会阻塞，此时可以做一些其它事情，例如处理其它请求。但是非阻塞IO需要主动轮询是否有数据需要处理，且这种轮询需要从用户态切换到内核态这，假如没有数据产生就会有很多空轮询，白白浪费cpu资源。</p><p>阻塞IO、非阻塞IO，要么需要开启更多线程去处理IO，要么需要从用户态切换到内核态轮询IO事件，那么有没有一种机制，用户程序只需要将请求提交给内核，由内核用少量的线程去监听，有事件就通知用户程序呢？这就是IO多路复用。</p><h3 id="ioio-multiplexing"><strong>IO多路复用(IO Multiplexing)</strong></h3><p><strong>IO多路复用机制是指一个线程处理多个IO流，多路是指网络连接，复用指的是同一个线程。</strong><br/>如果简单从图上看IO多路复用相比阻塞IO似乎并没有什么高明之处，假设服务只处理少量的连接，那么相比阻塞IO确实没有太大的提升，但如果连接数非常多，差距就会立竿见影。<br/>首先IO多路复用会提交一批需要监听的文件句柄（socket也是一种文件句柄）到内核，由内核开启一个线程负责监听，把轮询工作交给内核，当有事件发生时，由内核通知用户程序。这不需要用户程序开启更多的线程去处理连接，也不需要用户程序切换到内核态去轮询，用一个线程就能处理大量网络IO请求。<br/>redis底层采用的就是IO多路复用模型，实际上基本所有中间件在处理网络IO这一块都会使用到IO多路复用，如kafka,rocketmq等，所以本次学习之后对其它中间件的理解也是很有帮助的。</p><h2 id="selectpollepoll"><strong>select/poll/epoll</strong></h2><p>linux最开始提供的是select函数，方法如下：</p><pre class="language-text lang-text"><code class="language-text lang-text">select(int nfds, fd_set *r, fd_set *w, fd_set *e, struct timeval *timeout)
</code></pre>
<p>该方法需要传递3个集合，r,e,w分别表示读、写、异常事件集合。集合类型是bitmap，通过0/1表示该位置的fd(文件描述符，socket也是其中一种)是否关心对应读、写、异常事件。例如我们对fd为1和2的读事件关心，r参数的第1,2个bit就设置为1。</p><p><strong>用户进程调用select函数将关心的事件传递给内核系统，然后就会阻塞，直到传递的事件至少有一个发生时，方法调用会返回。内核返回时，同样把发生的事件用这3个参数返回回来，如r参数第1个bit为1表示fd为1的发生读事件，第2个bit依然为0，表示fd为2的没有发生读事件。用户进程调用时传递关心的事件，内核返回时返回发生的事件。</strong></p><p>select存在的问题：</p><ol start="1"><li>大小有限制。为1024，由于每次select函数调用都需要在用户空间和内核空间传递这些参数，为了提升拷贝效率，linux限制最大为1024。</li><li>这3个集合有相应事件触发时，会被内核修改，所以每次调用select方法都需要重新设置这3个集合的内容。</li><li>当有事件触发select方法返回，需要遍历集合才能找到就绪的文件描述符，例如传1024个读事件，只有一个读事件发生，需要遍历1024个才能找到这一个。</li><li>同样在内核级别，每次需要遍历集合查看有哪些事件发生，效率低下。</li></ol><p>poll函数对select函数做了一些改进</p><pre class="language-text lang-text"><code class="language-text lang-text">poll(struct pollfd *fds, int nfds, int timeout)

struct pollfd {
int fd;
short events;
short revents;
}
</code></pre>
<p>poll函数需要传一个pollfd结构数组，其中fd表示文件描述符，events表示关心的事件，revents表示发生的事件，当有事件发生时，内核通过这个参数返回回来。</p><p>poll相比select的改进：</p><ol start="1"><li>传不固定大小的数组，没有1024的限制了（问题1）</li><li>将关心的事件和实际发生的事件分开，不需要每次都重新设置参数（问题2）。例如poll数组传1024个fd和事件，实际只有一个事件发生，那么只需要重置一下这个fd的revent即可，而select需要重置1024个bit。</li></ol><p>poll没有解决select的问题3和4。另外，虽然poll没有1024个大小的限制，但每次依然需要在用户和内核空间传输这些内容，数量大时效率依然较低。</p><p>这几个问题的根本实际很简单，核心问题是select/poll方法对于内核来说是无状态的，内核不会保存用户调用传递的数据，所以每次都是全量在用户和内核空间来回拷贝，如果调用时传给内核就保存起来，有新增文件描述符需要关注就再次调用增量添加，有事件触发时就只返回对应的文件描述符，那么问题就迎刃而解了，这就是epoll做的事情。</p><p>epoll对应3个方法</p><pre class="language-text lang-text"><code class="language-text lang-text">int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
</code></pre>
<p>epoll_create负责创建一个上下文，用于存储数据，底层是用红黑树，以后的操作就都在这个上下文上进行。<br/>epoll_ctl负责将文件描述和所关心的事件注册到上下文。<br/>epoll_wait用于等待事件的发生，当有有事件触发，就只返回对应的文件描述符了。</p><p>select 底层是位图数组，限制就是会被数组长度限制，只有1024大小，内核轮询这个事件是否就绪，如果有的话内核态会把用户态的参数拷进内核空间然后检测每个的状态，select() 返回时，内核会把修改后的 fd_set（就绪标记）拷回用户空间，这样子的开销是比较大的，每次都需要扫描，拷贝。</p><p>poll底层的话是个链表，他的话解决了select的大小限制问题，但是还是没有解决用户态与内核态之间的问题</p><p>epoll的话他底层是个红黑树，他有三个函数，creat，ctl，wait，他的改进是用户在修改的时候，内核会把信息拷入内核数据结构，当内核返回就绪事件时候，epoll_wait会把准备好的事件批量拷贝到用户提供的缓冲区</p></div></div>]]></description><link>https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B</link><guid isPermaLink="true">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 17 Dec 2025 13:43:00 GMT</pubDate></item><item><title><![CDATA[ConcurrentHashMap底层原理]]></title><description><![CDATA[<link rel="preload" as="image" href="https://binzi.top/img/covers/image-20251126164915387.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/java/ConcurrentHashMap%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86">https://binzi.top/posts/java/ConcurrentHashMap%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86</a></blockquote><div><h1 id="concurrenthashmap">ConcurrentHashMap底层原理</h1><p>jdk1.7是数组加链表，它在对象中保存了一个<strong>Segment数组</strong>。每个Segment元素类似于一个Hashtable；这样，在执行put操作时首先根据hash算法定位到元素属于哪个Segment，然后对该Segment加锁即可，不同的Segment可以并发put。而Segment的锁实现其实是ReentrantLock</p><p>jdk1.8则是采用了数组+链表/红黑树，他舍弃了Segment数组，用的是CAS + synchronized</p><p>1.7put流程：</p><p>为输入的Key做Hash运算，得到hash</p><p>通过hash，定位到对应的Segment对象（根据hash值的高4位（根据Segment数量决定是高几位，如果是32个Segment，就是高5位），找到对应Segment）<br/>获取可重入锁<br/>再次通过hash值，定位到Segment当中数组的具体位置<br/>插入或覆盖HashEntry对象<br/>释放锁</p><p>1.7get流程：</p><p>为输入的key做Hash运算，得到hash<br/>通过hash，定位到对应的Segment对象<br/>再次通过hash，定位到Segment当中数组的具体位置</p><p>为什么get不需要上锁？因为使用了大量的volatile（Node中的val和next是volatile）</p><hr/><p>1.8put流程</p><p>检验key和value不能为null<br/>进入死循环<br/>如果第一次添加元素，初始化table，使用CAS保证线程安全，然后重新进入循环<br/>如果头节点为null，CAS将自己设置为头节点，成功则break，失败则重新进入循环<br/>如果头节点是ForwardingNode，说明这个桶正在进行扩容，则帮忙进行扩容，然后重新进入循环<br/>到了这里说明需要往桶下标的非头节点中添加元素，此时使用synchronized，锁对象是头节点<br/>再次确认链表头节点有没有被移动<br/>如果是链表，走链表put操作，如果有相同的key则更新，否则添加<br/>如果是红黑树，走红黑树put操作，如果有相同的key则更新，否则添加<br/>释放锁，判断是否需要树化，然后break<br/>使用累加单元数组进行size累加，判断是否需要扩容</p><p><img src="https://binzi.top/img/covers/image-20251126164915387.png" alt="image-20251126164915387" height="0" width="0"/></p><p>1.8get流程：无锁获取，因为Node[]和Node中的val和next有volatile</p><hr/><p>扩容</p><p>第一部分是构建一个 nextTable，它的容量是原来的两倍，这个操作是单线程完成的。</p><p>第二个部分是将原来 table 中的元素复制到 nextTable 中，主要是遍历复制的过程。遍历是<strong>从后往前</strong>遍历原table<br/>得到当前遍历的数组位置 i，然后利用 tabAt 方法获得 i 位置的元素：</p><p>如果这个位置为空，就在原 table 中的 i 位置放入 forwardNode 节点，这个也是触发并发扩容的关键；</p><p>如果这个位置是 Node 节点（fh&gt;=0），并且是链表的头节点，就把这个链表分裂成两个链表，把它们分别放在 nextTable 的 i 和 i+n 的位置上（桶下标不变的留在原地，变的放到 i + n 处）；处理完后头节点变成forwardNode（如果是链表，则复制值创建新节点对象扩容，如果是一个节点，则挪动元素扩容）</p><p>如果这个位置是 TreeBin 节点（fh&lt;0），也做一个反序处理，并且判断是否需要 untreefi，把处理的结果分别放在 nextTable 的 i 和 i+n 的位置上；处理完后头节点变成forwardNode</p><p>遍历所有的节点，就完成复制工作，这时让 nextTable 作为新的 table，并且更新 sizeCtl 为新容量的 0.75 倍 ，完成扩容。</p><p><strong>扩容会把正在扩容的头结点设置成一个特殊结点，然后从get，put的角度说</strong></p><p>扩容时有其余线程来get：如果头节点是forwardNode，那么就去新table中get，否则，在原table中查询，又因为迁移链表时是深拷贝扩容，所以get能查找到对应元素，不会出现并发问题</p><p>扩容时有其余线程来put：<br/>如果要put的桶下标还不在扩容中，直接put<br/>如果put的桶下标正在扩容中，只能阻塞<br/>如果put的桶下标是forwarding，此时不能往新table中put，因为此时新table还没扩容完成，put可能产生并发问题，此时会去协助扩容</p></div></div>]]></description><link>https://binzi.top/posts/java/ConcurrentHashMap%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86</link><guid isPermaLink="true">https://binzi.top/posts/java/ConcurrentHashMap%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 28 Nov 2025 05:05:00 GMT</pubDate></item><item><title><![CDATA[SpringIOC]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/java/SpringIOC">https://binzi.top/posts/java/SpringIOC</a></blockquote><div><h3 id="-ioc">什么是 IoC?</h3><p>IoC （Inversion of Control ）即控制反转/反转控制。它是一种思想不是一个技术实现。描述的是：Java 开发领域对象的创建以及管理的问题。</p><p>例如：现有类 A 依赖于类 B</p><ul><li><strong>传统的开发方式</strong> ：往往是在类 A 中手动通过 new 关键字来 new 一个 B 的对象出来</li><li><strong>使用 IoC 思想的开发方式</strong> ：不通过 new 关键字来创建对象，而是通过 IoC 容器(Spring 框架) 来帮助我们实例化对象。我们需要哪个对象，直接从 IoC 容器里面去取即可。</li></ul><p>从以上两种开发方式的对比来看：我们 “丧失了一个权力” (创建、管理对象的权力)，从而也得到了一个好处（不用再考虑对象的创建、管理等一系列的事情）</p><p><strong>为什么叫控制反转?</strong></p><ul><li><strong>控制</strong> ：指的是对象创建（实例化、管理）的权力</li><li><strong>反转</strong> ：控制权交给外部环境（IoC 容器）</li></ul><h3 id="ioc-">IoC 解决了什么问题?</h3><p>IoC 的思想就是两方之间不互相依赖，由第三方容器来管理相关资源。这样有什么好处呢？</p><ol start="1"><li>对象之间的耦合度或者说依赖程度降低；</li><li>资源变的容易管理；比如你用 Spring 容器提供的话很容易就可以实现一个单例</li></ol><h3 id="iocdi">IOC和DI的区别？</h3><p><strong>IoC（控制反转, Inversion of Control）</strong> 是一种设计思想/原则：把对象的控制权（创建、生命周期、依赖获取等）从应用程序代码中“反转”到框架或容器。也就是说：<strong>不是对象去主动创建或查找依赖，而由外部容器来负责提供（控制）这些依赖。</strong></p><p><strong>DI（依赖注入, Dependency Injection）</strong> 是实现 IoC 的<strong>一种具体技术/方式</strong>——把依赖（对象）“注入”到目标对象中。常见注入方式：构造器注入、Setter 注入、字段注入。</p><p>IoC 是设计思想，DI 是具体的实现方式；</p><p>IoC 是理论，DI 是实践；</p><h3 id="bean-">Bean 的生命周期</h3><p>第一个阶段是实例化。Spring 容器会根据 BeanDefinition，通过反射调用 Bean 的构造方法创建对象实例。如果有多个构造方法，Spring 会根据依赖注入的规则选择合适的构造方法。</p><p>第二阶段是属性赋值。这个阶段 Spring 会给 Bean 的属性赋值，包括通过 <code>@Autowired</code>、<code>@Resource</code> 这些注解注入的依赖对象，以及通过 <code>@Value</code> 注入的配置值。</p><p>第三阶段是初始化。这个阶段会依次执行：</p><ul><li><code>@PostConstruct</code> 标注的方法</li><li>InitializingBean 接口的 afterPropertiesSet 方法</li><li>通过 <code>@Bean</code> 的 initMethod 指定的初始化方法</li></ul><p>第四阶段是使用 Bean。比如我们的 Controller 调用 Service，Service 调用 DAO。</p><p>最后是销毁阶段。当容器关闭或者 Bean 被移除的时候，会依次执行：</p><ul><li><code>@PreDestroy</code> 标注的方法</li><li>DisposableBean 接口的 destroy 方法</li><li>通过 <code>@Bean</code> 的 destroyMethod 指定的销毁方法</li></ul><h3 id="springioc">项目启动时Spring的IoC会做什么？</h3><p>第一件事是扫描和注册 Bean。IoC 容器会根据我们的配置，比如 <code>@ComponentScan</code> 指定的包路径，去扫描所有标注了 <code>@Component</code>、<code>@Service</code>、<code>@Controller</code> 这些注解的类。然后把这些类的元信息包装成 BeanDefinition 对象，注册到容器的 BeanDefinitionRegistry 中。这个阶段只是收集信息，还没有真正创建对象。</p><p>第二件事是 Bean 的实例化和注入。这是最核心的过程，IoC 容器会按照依赖关系的顺序开始创建 Bean 实例。对于单例 Bean，容器会通过反射调用构造方法创建实例，然后进行属性注入，最后执行初始化回调方法。</p></div></div>]]></description><link>https://binzi.top/posts/java/SpringIOC</link><guid isPermaLink="true">https://binzi.top/posts/java/SpringIOC</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 24 Nov 2025 10:51:00 GMT</pubDate></item><item><title><![CDATA[HashMap]]></title><description><![CDATA[<link rel="preload" as="image" href="https://binzi.top/img/covers/image-20251122172610739.png"/><link rel="preload" as="image" href="https://binzi.top/img/covers/image-20251122175757222.png"/><link rel="preload" as="image" href="https://binzi.top/img/covers/image-20251123013013969.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/java/HashMap">https://binzi.top/posts/java/HashMap</a></blockquote><div><h1 id="hashmap">HashMap</h1><p>JDK 7 中 HashMap 的数据结构是<code>数组</code>+<code>链表</code>。</p><p>JDK 8 中 HashMap 的数据结构是<code>数组</code>+<code>链表</code>+<code>红黑树</code>。</p><p>​<strong>它在链表长度大于8且数组长度大于64时候会把链表转换成红黑树</strong></p><p>HashMap 的初始容量是 16，随着元素的不断添加，HashMap 就需要进行扩容，阈值是<code>capacity * loadFactor</code>，capacity 为容量，loadFactor 为负载因子，默认为 0.75。</p><p>扩容后的数组大小是原来的 2 倍，然后把原来的元素重新计算哈希值，放到新的数组中。</p><p>负载因子（load factor）是一个介于 0 和 1 之间的数值，用于衡量哈希表的填充程度。它表示哈希表中已存储的元素数量与哈希表容量之间的比例。</p><ul><li>负载因子过高（接近 1）会导致哈希冲突增加，影响查找、插入和删除操作的效率。</li><li>负载因子过低（接近 0）会浪费内存，因为哈希表中有大量未使用的空间。</li></ul><h3 id="hashmap-put"><strong>HashMap 的put过程：</strong></h3><p>1.他会先对key计算扰动hash，通过key.hashCode() ^ (key.hashCode() &gt;&gt;&gt; 16)计算，这样的目的是为了高16位的熵混进去低16位，减少碰撞。</p><p>2.判断当前是不是第一次添加 是的话就创建一个默认长度16，负载因子0.75的数组。</p><p>3.开始计算数组下标index，通过前面计算的hash &amp; (n - 1)来确定位置 这里的n是2的次幂</p><p>4.然后获取当前数组中对应的数据</p><p>5.数组为空，底层会创建一个键值对对象，直接放进去就行了</p><p>6.不为空</p><p>​6.1.如果当前插入的键值对与数组的键值对哈希值一样，也就是插入的键值与插入后的键值一样就把调整当前元素指针，在后面进行替换操作</p><p>​6.1.已经是TreeNode，走红黑树的添加或更新逻辑</p><p>​6.2.是普通Node，走链表的添加或更新逻辑，如果达到树化条件，走树化逻辑</p><p>添加后、返回前检查容量是否达到扩容阈值，如果达到，进行扩容</p><p><img src="https://binzi.top/img/covers/image-20251122172610739.png" alt="image-20251122172610739" height="0" width="0"/> <img src="https://binzi.top/img/covers/image-20251122175757222.png" alt="image-20251122175757222"/></p><h3 id="hashmap2n">为什么HashMap的容量是2的n次方</h3><h4 id="1-n---1--hash---n">1.用位运算 <code>(n - 1) &amp; hash</code> 快速代替取模 <code>% n</code></h4><p>​在n为偶数时候 n - 1为奇数 （n - 1）&amp; hash等价于 hash % n 并且位运算比取模快得多</p><h4 id="2">2.保证哈希值在桶上的分布尽可能均匀，避免部分桶永远用不到</h4><p>​2 幂次方刚好是偶数，偶数-1 是奇数，奇数的二进制最后一位是 1，也就保证了 <code>hash &amp;(length-1)</code> 的最后一位可能为 0，也可能为 1（取决于 hash 的值），这样可以保证哈希值的均匀分布。</p><h4 id="3-oldcap-jdk18-">3.方便扩容时用 <code>oldCap</code> 这一位快速判断元素新位置（JDK1.8 的优化）</h4><p>桶里每个节点的新位置只和这一位有关：</p><pre class="language-java lang-java"><code class="language-java lang-java">// oldCap 一定是 2 的幂，比如 16(10000b)
if ((hash &amp; oldCap) == 0) {
    // 这位是 0：留在原来的 index
    newIndex = oldIndex;
} else {
    // 这位是 1：去到 index + oldCap
    newIndex = oldIndex + oldCap;
}
</code></pre>
<p>​</p><p>​JDK 1.8 扩容时利用 <code>hash &amp; oldCap</code> 这位来判断元素是留在原索引还是移动到 <code>index + oldCap</code>，极大简化了 rehash 的过程，这个优化也依赖容量是 2 的幂。</p><h3 id="hashmapequalhashcode">重写HashMap的equal和hashcode方法需要注意什么？</h3><ul><li>如果o1.equals(o2)，那么o1.hashCode() == o2.hashCode()总是为true的。</li><li>如果o1.hashCode() == o2.hashCode()，并不意味着o1.equals(o2)会为true。</li></ul><h2 id="hashmap-jdk18">HashMap 的扩容机制和扩容过程（JDK1.8）</h2><h4 id="1">1）何时扩容？</h4><ul><li>有个阈值：<code>threshold = capacity * loadFactor</code>
<ul><li>默认：<code>capacity = 16</code>，<code>loadFactor = 0.75</code></li><li>所以初始 threshold = 12</li></ul></li><li>当 <code>size &gt; threshold</code> 时，进行扩容：
<ul><li><strong>数组长度 ×2</strong>，比如 16 -&gt; 32。</li></ul></li></ul><h4 id="2resize">2）扩容过程（<code>resize()</code>）</h4><ol start="1"><li>创建一个新数组 <code>newTable</code>，长度 = 原来 2 倍。</li><li>遍历旧数组每个桶：
<ul><li>如果桶为空，跳过。</li><li>如果桶里只有一个节点，直接搬到新数组对应位置。</li><li>如果桶是红黑树，拆成两棵或调整后搬到新数组中。</li><li>如果桶是链表：
<ul><li>JDK1.8 做了一个优化：
<ul><li>利用 <code>e.hash &amp; oldCap</code> 的结果来判断节点在新数组中的位置：
<ul><li>如果为 0：节点留在原位置 <code>index</code>。</li><li>如果为 1：节点去到 <code>index + oldCap</code>。</li></ul></li></ul></li><li>这样链表可以拆成两个链表，过程更快，也避免 1.7 里“倒序 + 死循环”的问题。</li></ul></li></ul></li></ol><p>扩容是最重的操作：<strong>需要重新分布所有元素</strong>，所以尽量别频繁扩容（比如一开始就知道大概数量，可以传一个合适的 initialCapacity）。</p><h2 id="hashmap--hash-">HashMap 出现 Hash 冲突时怎么办？何时链表进化成红黑树？何时退化？</h2><h4 id="1hash-">1）hash 冲突的解决方式</h4><ul><li>HashMap 使用的是 <strong>“拉链法”</strong>：
<ul><li>数组 + 链表/红黑树。</li><li>同一个桶上的多个元素通过 next 指针连成一条链，或组织成一棵红黑树。</li></ul></li></ul><h4 id="2---jdk18">2）链表 -&gt; 红黑树（树化）的条件（JDK1.8）</h4><ul><li>满足 <strong>两个条件</strong> 才会树化：</li></ul><ol start="1"><li>单个桶中的链表长度 ≥ <code>TREEIFY_THRESHOLD</code>（默认 8）。</li><li>整个 table 的长度 ≥ <code>MIN_TREEIFY_CAPACITY</code>（默认 64）。</li></ol><blockquote><p>不满足第二个条件时，会优先选择 <strong>扩容</strong> 而不是树化。</p></blockquote>
<p><strong>原因</strong>：</p><ul><li>如果表太小，说明整体碰撞重；这时扩容可以让数据分摊到更多桶里，比急着树化更划算。</li></ul><h4 id="3---">3）红黑树 -&gt; 链表（退化）的条件</h4><ul><li>当树中的节点个数 ≤ <code>UNTREEIFY_THRESHOLD</code>（默认 6）时，</li><li><p><strong>remove树节点时，如果root、root.left、root.right、root.left.left有一个为null（在移除之前检查），也会退化成链表</strong></p><p><img src="https://binzi.top/img/covers/image-20251123013013969.png" alt="image-20251123013013969"/></p></li></ul><h3 id="jdk8">JDK8为什么改用尾插法？</h3><p>JDK8引入了红黑树，那么就可以插入时顺便遍历一遍当前桶有多少元素，决定是否转为红黑树<br/>避免多线程下扩容可能产生的死循环</p><h4 id="">扩容的时候每个节点都要进行位运算吗？</h4><p>不需要。HashMap 会通过 <code>(e.hash &amp; oldCap)</code> 来判断节点是否需要移动，0 的话保留原索引；1 才需要移动到新索引（原索引 + oldCap）。</p><p>这样就避免了 hashCode 的重新计算，大大提升了扩容的性能。</p><p>所以，哪怕有几十万条数据，可能只有一半的数据才需要移动到新位置。另外，位运算的计算速度非常快，因此，尽管扩容操作涉及到遍历整个哈希表并对每个节点进行判断，但这部分操作的计算成本是相对较低的。</p><h2 id="stringhashcode31">String的hashCode()如何设计的，为啥每次乘的是31</h2><p>字符串中的每个字符都可以表现为一个数字，称为Si，其中i的范围是0 ~ n - 1<br/>散列公式为：S0 * 31 ^ (n - 1) + S1 * 31 ^ (n - 2) + S2 * 31 ^ (n -3) + … + Sn-1 * 31 ^ (0)<br/>31代入公式有较好的散列特性，并且31 * h可以被优化为 h &lt;&lt; 5 - h<br/>但这算法时间复杂度o(n)</p><h2 id="">哈希冲突解决方法:</h2><p>拉链法：哈希冲突时，同一个哈希槽拉成一个链表，HashMap采用该方法<br/>开放寻址：寻找其他位置，这就保证了一个哈希槽最多存有一个元素<br/>线性探测：哈希冲突时，按顺序往后查找，直到找到第一个为空的槽位，ThreadLocalMap采用该方法<br/>二次探测：与线性探测类似，但有规律，比如每次步长是1，2，4，8，16。。。<br/>再哈希法：哈希冲突时，换个哈希函数再次哈希</p></div></div>]]></description><link>https://binzi.top/posts/java/HashMap</link><guid isPermaLink="true">https://binzi.top/posts/java/HashMap</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sun, 23 Nov 2025 06:19:00 GMT</pubDate></item><item><title><![CDATA[Volatile原理]]></title><description><![CDATA[<link rel="preload" as="image" href="https://binzi.top/img/covers/image-20251122222435220.png"/><link rel="preload" as="image" href="https://binzi.top/img/covers/image-20251122222526124-1769175058736-8.png"/><link rel="preload" as="image" href="https://binzi.top/img/covers/image-20251122222611385-1769175065940-10.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/java/Volatile%E5%8E%9F%E7%90%86">https://binzi.top/posts/java/Volatile%E5%8E%9F%E7%90%86</a></blockquote><div><p>Volatile原理</p><p><img src="https://binzi.top/img/covers/image-20251122222435220.png" alt="image-20251122222435220" height="0" width="0"/></p><p><img src="https://binzi.top/img/covers/image-20251122222526124-1769175058736-8.png" alt="image-20251122222526124"/></p><p><img src="https://binzi.top/img/covers/image-20251122222611385-1769175065940-10.png" alt="image-20251122222611385"/></p></div></div>]]></description><link>https://binzi.top/posts/java/Volatile%E5%8E%9F%E7%90%86</link><guid isPermaLink="true">https://binzi.top/posts/java/Volatile%E5%8E%9F%E7%90%86</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 22 Nov 2025 16:56:00 GMT</pubDate></item><item><title><![CDATA[MySQL 联合索引的最左匹配原则]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL%20%E8%81%94%E5%90%88%E7%B4%A2%E5%BC%95%E7%9A%84%E6%9C%80%E5%B7%A6%E5%8C%B9%E9%85%8D%E5%8E%9F%E5%88%99">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL%20%E8%81%94%E5%90%88%E7%B4%A2%E5%BC%95%E7%9A%84%E6%9C%80%E5%B7%A6%E5%8C%B9%E9%85%8D%E5%8E%9F%E5%88%99</a></blockquote><div><h1 id="mysql-">MySQL 联合索引的最左匹配原则</h1><p><strong>执行计划基础知识</strong></p><p><strong>possible_keys</strong>：可能用到的索引</p><p><strong>key</strong>：实际用到的索引</p><p><strong>type</strong>:</p><ol start="1"><li>ref：当通过普通的二级索引列与常量进行等值匹配的方式 询某个表时</li><li>const：当我们根据主键或者唯一得二级索引列与常数进行等值匹配时，对单表的访问方法就是 const</li><li>range:如果使用索引获取某些单点扫描区间的记录。</li><li>index：当可以使用覆盖 ，但需要扫描全部的索引记录时。</li></ol><p><strong>Extra</strong>:</p><ol start="1"><li>Using index 索引覆盖</li><li>Using Where 当某个搜索条件需要在 server 层进行判断时</li><li>Using index for skip scan 跳跃扫描</li><li>Using index condtion 索引下推</li></ol><h2 id="">最左匹配原则</h2><p>最左优先，以最左边的为起点任何连续的索引都能匹配上。同时遇到范围查询(&gt;、&lt;、between、like)就会停止匹配。</p><p>比如有联合索引 [a、b、c]，where 过滤条件中哪些排列组合可以用到索引？（比如这种：where a=xxx b=xxx and c=xxx）</p><p>以下排列组合都会走索引： a、ab、ac、ba、ca、abc、acb、bac、bca、cab、cba。 必须有一个 a，排列组合中的顺序会被优化器优化，所以不用关心顺序。</p><p>以下排列组合不会走索引： b、c、bc、cb。 因为没有 a。</p><p>关于范围查询： a=xxx and b&lt;10 and b &gt; 5 and c =xxx，c 字段用不到索引，因为 b 是一个范围查询，遇到范围查询就停止了。</p><p>by -<a href="https://juejin.cn/post/7283832557502693413">https://juejin.cn/post/7283832557502693413</a></p></div></div>]]></description><link>https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL%20%E8%81%94%E5%90%88%E7%B4%A2%E5%BC%95%E7%9A%84%E6%9C%80%E5%B7%A6%E5%8C%B9%E9%85%8D%E5%8E%9F%E5%88%99</link><guid isPermaLink="true">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL%20%E8%81%94%E5%90%88%E7%B4%A2%E5%BC%95%E7%9A%84%E6%9C%80%E5%B7%A6%E5%8C%B9%E9%85%8D%E5%8E%9F%E5%88%99</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 21 Nov 2025 17:25:00 GMT</pubDate></item><item><title><![CDATA[redis做为缓存，mysql的数据如何与redis进行同步呢？（双写一致性）]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/redis%E5%81%9A%E4%B8%BA%E7%BC%93%E5%AD%98%EF%BC%8Cmysql%E7%9A%84%E6%95%B0%E6%8D%AE%E5%A6%82%E4%BD%95%E4%B8%8Eredis%E8%BF%9B%E8%A1%8C%E5%90%8C%E6%AD%A5%E5%91%A2%EF%BC%9F%EF%BC%88%E5%8F%8C%E5%86%99%E4%B8%80%E8%87%B4%E6%80%A7%EF%BC%89">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/redis%E5%81%9A%E4%B8%BA%E7%BC%93%E5%AD%98%EF%BC%8Cmysql%E7%9A%84%E6%95%B0%E6%8D%AE%E5%A6%82%E4%BD%95%E4%B8%8Eredis%E8%BF%9B%E8%A1%8C%E5%90%8C%E6%AD%A5%E5%91%A2%EF%BC%9F%EF%BC%88%E5%8F%8C%E5%86%99%E4%B8%80%E8%87%B4%E6%80%A7%EF%BC%89</a></blockquote><div><h2 id="redismysqlredis"><strong>redis做为缓存，mysql的数据如何与redis进行同步呢？（双写一致性）</strong></h2><p>在项目中，我们主要通过 <strong>Redisson 的读写锁机制</strong> 来保证数据的<strong>强一致性</strong>。</p><p>当执行<strong>读取操作</strong>时，我们会加上<strong>读锁（共享锁）</strong>，这样可以做到：</p><ul><li>读读不互斥；</li><li>读写互斥。</li></ul><p>而在<strong>更新数据</strong>时，我们会加上<strong>写锁（排他锁）</strong>，此时写写、读写都互斥。<br/>这样可以确保在写入数据库的同时，其他线程无法读取缓存数据，从而避免出现<strong>脏读</strong>或<strong>不一致</strong>的情况。<br/>这里需要注意的一点是：<strong>读写方法必须使用同一把锁</strong>，才能真正保证互斥关系成立。</p><hr/><p>除了使用分布式锁，我们也考虑过使用 <strong>“延迟双删”策略</strong> 来实现<strong>最终一致性</strong>。</p><p>具体做法是：</p><ol start="1"><li>先删除缓存；</li><li>再更新数据库；</li><li>最后延时一段时间后，再删除一次缓存。</li></ol><p>不过，延迟的时间点很难精确把握。在延时窗口内仍可能出现<strong>脏数据</strong>问题，无法满足强一致性场景，因此我们最终没有采用这种方式。</p><hr/><p>在我最近做的项目中（例如简历中提到的 <em>xxxx 功能</em>），<br/>系统对数据一致性的实时性要求没有那么高，可以接受一定的同步延时。</p><p>因此我们采用了 <strong>阿里巴巴的 Canal 组件</strong> 来实现<strong>最终一致性的数据同步</strong>。</p><p>Canal 的原理是：</p><ul><li>将自己伪装成 MySQL 的从节点；</li><li>通过监听 <strong>binlog（数据库增删改日志）</strong> 来捕获数据变更；</li><li>Canal 服务解析 binlog 后，将变更数据推送给客户端；</li><li>客户端根据变更内容更新或删除 Redis 缓存。</li></ul><p>这种方式的优点是：</p><ul><li><strong>不需要修改业务代码</strong>；</li><li>同步过程自动化；</li><li>能在大多数业务场景下保证“最终一致性”。</li></ul></div></div>]]></description><link>https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/redis%E5%81%9A%E4%B8%BA%E7%BC%93%E5%AD%98%EF%BC%8Cmysql%E7%9A%84%E6%95%B0%E6%8D%AE%E5%A6%82%E4%BD%95%E4%B8%8Eredis%E8%BF%9B%E8%A1%8C%E5%90%8C%E6%AD%A5%E5%91%A2%EF%BC%9F%EF%BC%88%E5%8F%8C%E5%86%99%E4%B8%80%E8%87%B4%E6%80%A7%EF%BC%89</link><guid isPermaLink="true">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/redis%E5%81%9A%E4%B8%BA%E7%BC%93%E5%AD%98%EF%BC%8Cmysql%E7%9A%84%E6%95%B0%E6%8D%AE%E5%A6%82%E4%BD%95%E4%B8%8Eredis%E8%BF%9B%E8%A1%8C%E5%90%8C%E6%AD%A5%E5%91%A2%EF%BC%9F%EF%BC%88%E5%8F%8C%E5%86%99%E4%B8%80%E8%87%B4%E6%80%A7%EF%BC%89</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 14 Nov 2025 13:27:00 GMT</pubDate></item><item><title><![CDATA[Redis分布式锁redisson看门狗机制]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81redisson%E7%9C%8B%E9%97%A8%E7%8B%97%E6%9C%BA%E5%88%B6">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81redisson%E7%9C%8B%E9%97%A8%E7%8B%97%E6%9C%BA%E5%88%B6</a></blockquote><div><h3 id="redis">Redis分布式锁方案</h3><p>使用 Redis 的 <code>SET key value NX</code> EX实现分布式锁时，主要问题是锁过期时间不好设置：如果设置过短，业务未完成锁就过期；设置过长，会降低系统并发度。为此可以使用 Redisson 封装的分布式锁，它内部通过「看门狗机制」自动续期锁（默认锁 30 秒，每 10 秒续期一次），同时通过 Lua 脚本保证加解锁的原子性，从而确保分布式环境下的并发安全。</p><h3 id="redisson">Redisson分布式锁自动续约机制（看门狗）</h3><p>线程1尝试tryLock锁未指定锁的过期时间 获取成功后会自动启动看门狗机制</p><p><strong>初始加锁</strong>：</p><p>​当调用 <code>lock()</code> 时，Redisson 会通过 Lua 脚本向 Redis 发送加锁命令，设置锁的初始过期时间为 <code>LockWatchdogTimeout</code>（默认 30 秒）。</p><p>​锁的数据结构为 <code>Hash</code>，键为锁名称，<code>field</code> 为线程 ID，<code>value</code> 为重入次数。</p><p><strong>启动定时任务</strong>：</p><p>​加锁成功后，Redisson 会启动一个<strong>后台定时任务</strong>（通过 Netty 的 <code>HashedWheelTimer</code> 实现），每隔 10 秒执行一次。</p><h5 id="">续期逻辑：</h5><p>​定时任务通过lua脚本检查锁是否存在并且被当前线程持有</p><p>​若条件满足，则通过 <code>pexpire</code> 命令将锁的过期时间重置为 <code>LockWatchdogTimeout</code>（30 秒）。</p><p>​续期成功后，继续等待下一个检查周期。</p><p><strong>锁释放时终止续期</strong>：</p><p>​当调用 <code>unlock()</code> 释放锁时，会同时取消续期的定时任务，避免无限续期。</p></div></div>]]></description><link>https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81redisson%E7%9C%8B%E9%97%A8%E7%8B%97%E6%9C%BA%E5%88%B6</link><guid isPermaLink="true">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81redisson%E7%9C%8B%E9%97%A8%E7%8B%97%E6%9C%BA%E5%88%B6</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 14 Nov 2025 12:24:00 GMT</pubDate></item><item><title><![CDATA[LeetCode Hot 100 - 24. 两两交换链表中的节点]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/24.%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9">https://binzi.top/posts/%E7%AE%97%E6%B3%95/24.%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9</a></blockquote><div><h1 id="leetcode-24-httpsleetcodecnproblemsswap-nodes-in-pairs"><a href="https://leetcode.cn/problems/swap-nodes-in-pairs/">LeetCode 24. 两两交换链表中的节点</a></h1><h2 id="">题目大意</h2><p>给定一个单链表，需要将链表中的节点按顺序两两交换，并返回交换后新的链表头节点。禁止只交换节点的数值，必须真实地调整指针。当链表长度为奇数时，最后一个节点保持原状。</p><h2 id="">示例</h2><ul><li>输入：<code>head = [1,2,3,4]</code></li><li>输出：<code>[2,1,4,3]</code></li></ul><p>当输入为 <code>head = [1,2,3]</code> 时，结果应为 <code>[2,1,3]</code>，因为第三个节点无法参与交换。</p><h2 id="">代码实现</h2><pre class="language-java lang-java"><code class="language-java lang-java">/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode dummy = new ListNode(0, head);//初始化 0-&gt;1-&gt;2-&gt;3-&gt;4
        ListNode curr = dummy.next;
        ListNode node0 = dummy;
        ListNode node1 = curr;
      
        while (node1 != null &amp;&amp; node1.next != null) {//至少为两个节点
            ListNode node2 = node1.next;
            ListNode node3 = node2.next;

            node0.next = node2;//0-&gt;2
            node1.next = node3;//1-&gt;3
            node2.next = node1;//2-&gt;1

            node0 = node1;//交换 在下一次的循环中1就是0
            node1 = node3;//3就是1
        }

        return dummy.next;
    }
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/24.%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/24.%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 11 Nov 2025 07:00:00 GMT</pubDate></item><item><title><![CDATA[Redis的数据过期淘汰策略]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E7%9A%84%E6%95%B0%E6%8D%AE%E8%BF%87%E6%9C%9F%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E7%9A%84%E6%95%B0%E6%8D%AE%E8%BF%87%E6%9C%9F%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5</a></blockquote><div><h3 id="redis-"><strong>Redis 的数据过期策略</strong></h3><p>Redis 采用 <strong>「惰性删除 + 定期删除」</strong> 两种策略相结合的方式，在 <strong>CPU 性能开销</strong> 与 <strong>内存占用</strong> 之间取得平衡。</p><hr/><h4 id="lazy-expiration"><strong>一、惰性删除（Lazy Expiration）</strong></h4><p>当客户端访问或修改某个键时，Redis 会在操作前调用 <code>expireIfNeeded()</code> 函数检查该键是否已过期：</p><ul><li><strong>若已过期</strong>：<br/>Redis 会立即删除该键，然后向客户端返回 <code>null</code>。<br/>删除方式由配置项 <code>lazyfree-lazy-expire</code> 决定：
<ul><li>若开启（<code>yes</code>），则采用 <strong>异步删除</strong>；</li><li>若关闭（<code>no</code>），则采用 <strong>同步删除</strong>。</li></ul></li><li><strong>若未过期</strong>：<br/>直接返回该键对应的正常值，不做任何额外处理。</li></ul><p>这种方式的优点是 <strong>删除时机精准、不会浪费 CPU 时间</strong>；<br/>缺点是：<strong>若某些过期键一直未被访问，就无法被清除，会长期占用内存</strong>。</p><hr/><h4 id="active-expiration-cycle"><strong>二、定期删除（Active Expiration Cycle）</strong></h4><p>为了解决惰性删除的不足，Redis 还会周期性地扫描部分键空间，清除已过期的数据。</p><h5 id="1-"><strong>1. 检查频率</strong></h5><p>Redis 默认每秒执行 <strong>10 次过期扫描</strong>，这一频率由配置文件 <code>redis.conf</code> 中的参数 <code>hz</code> 控制：</p><pre class=""><code class="">hz 10
</code></pre>
<p>即 Redis 每隔约 <strong>100 毫秒</strong> 触发一次过期检测。</p><hr/><h5 id="2-"><strong>2. 检查机制</strong></h5><p>Redis 并不会遍历所有键，而是采用「<strong>随机抽查</strong>」机制来控制 CPU 占用。<br/>该逻辑由 <code>expire.c</code> 文件中的 <code>activeExpireCycle()</code> 函数实现，核心过程如下：</p><ol start="1"><li>从每个数据库的「过期字典」中 <strong>随机抽取 20 个 key</strong>（数量由常量 <code>ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP = 20</code> 决定）；</li><li>检查这 20 个 key 是否过期，并删除已过期的 key；</li><li>如果本轮抽查中，<strong>超过 25%（即 5 个）键过期</strong>，说明系统中可能仍存在大量过期数据，Redis 会继续重复步骤 1，进行新一轮检查；</li><li>若过期比例低于 25%，则认为当前负载正常，Redis 会停止本轮扫描，等待下一次周期执行。</li></ol><h3 id="redis-">介绍一下Redis 内存淘汰策略</h3><p>在 32 位操作系统中，maxmemory 的默认值是 3G，因为 32 位的机器最大只支持 4GB 的内存，而系统本身就需要一定的内存资源来支持运行，所以 32 位操作系统限制最大 3 GB 的可用内存是非常合理的，这样可以避免因为内存不足而导致 Redis 实例崩溃。</p><p><em>1、不进行数据淘汰的策略：</em></p><ul><li><strong>noeviction</strong>（Redis3.0之后，默认的内存淘汰策略） ：它表示当运行内存超过最大设置内存时，不淘汰任何数据，这时如果有新的数据写入，会报错通知禁止写入，不淘汰任何数据，但是如果没用数据写入的话，只是单纯的查询或者删除操作的话，还是可以正常工作。</li></ul><p><em>2、进行数据淘汰的策略：</em></p><p>针对「进行数据淘汰」这一类策略，又可以细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。</p><ul><li>在设置了过期时间的数据中进行淘汰：
<ul><li><strong>volatile-random</strong>：随机淘汰设置了过期时间的任意键值；</li><li><strong>volatile-ttl</strong>：优先淘汰更早过期的键值。</li><li><strong>volatile-lru</strong>（Redis3.0 之前，默认的内存淘汰策略）：淘汰所有设置了过期时间的键值中，最久未使用的键值；</li><li><strong>volatile-lfu</strong>（Redis 4.0 后新增的内存淘汰策略）：淘汰所有设置了过期时间的键值中，最少使用的键值；</li></ul></li><li>在所有数据范围内进行淘汰：
<ul><li><strong>allkeys-random</strong>：随机淘汰任意键值;</li><li><strong>allkeys-lru</strong>：淘汰整个键值中最久未使用的键值；</li><li><strong>allkeys-lfu</strong>（Redis 4.0 后新增的内存淘汰策略）：淘汰整个键值中最少使用的键值。</li></ul></li></ul></div></div>]]></description><link>https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E7%9A%84%E6%95%B0%E6%8D%AE%E8%BF%87%E6%9C%9F%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5</link><guid isPermaLink="true">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis%E7%9A%84%E6%95%B0%E6%8D%AE%E8%BF%87%E6%9C%9F%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 27 Oct 2025 11:17:00 GMT</pubDate></item><item><title><![CDATA[缓存穿透，雪崩，击穿]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F%EF%BC%8C%E9%9B%AA%E5%B4%A9%EF%BC%8C%E5%87%BB%E7%A9%BF">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F%EF%BC%8C%E9%9B%AA%E5%B4%A9%EF%BC%8C%E5%87%BB%E7%A9%BF</a></blockquote><div><h3 id="">缓存穿透</h3><p>缓存穿透指的是：<strong>一个缓存和数据库中都不存在的数据</strong>，由于数据库无法写入缓存，导致频繁请求直接打到数据库，从而造成数据库压力过大甚至被“打穿”。</p><p>常见的解决方法包括：<strong>缓存空值</strong> 或 <strong>布隆过滤器</strong>。</p><p>布隆过滤器由一个<strong>全 0 的位图数组</strong>和 <strong>n 个哈希函数</strong>组成。当一个数据在请求缓存前，会先经过布隆过滤器。过滤器会通过 n 个哈希函数计算出 n 个哈希值，再分别对数组长度取模，并将对应的数组下标置为 1。</p><p>查询时，只需要查看位图数组中这 n 个位置是否全部为 1：</p><ul><li>若存在某个位为 0，则说明该数据<strong>一定不存在</strong>于数据库中；</li><li>若全部为 1，则说明该数据<strong>可能存在</strong>，但<strong>不一定真的存在</strong>，因为可能出现哈希碰撞。</li></ul><p>因此，布隆过滤器判断“存在”时并不能保证数据真的存在，但判断“不存在”时一定准确。</p><h3 id="">缓存雪崩</h3><p>缓存雪崩指的是：<strong>大量的缓存 Key 在同一时间失效</strong>，或者 <strong>Redis 服务器宕机</strong>，导致所有请求同时打到数据库，造成数据库瞬时压力过大，甚至宕机的情况。</p><p><strong>解决方法：</strong><br/>为缓存设置 <strong>固定过期时间 + 随机过期时间</strong>，让不同 Key 的过期时间错开，避免在同一时间集中失效。</p><h3 id="">缓存击穿</h3><p>缓存击穿指的是一个热点key设置了时间了过后 时间过期了 恰好这个时候大量请求打到数据库 让数据库压力大</p><p><strong>解决方案：</strong></p><h5 id="">方案一：互斥锁（分布式锁）</h5><p>当缓存失效时，不立即去加载数据库，而是<strong>先使用 Redis 的 <code>SETNX</code> 命令设置互斥锁</strong>。</p><ul><li>若设置成功（即当前线程获得锁），则执行 <code>load db</code> 操作并回填缓存；</li><li>若设置失败（说明已有线程在加载数据），则稍后<strong>重试获取缓存</strong>。</li></ul><p>优点：保证数据一致性。<br/>缺点：性能稍差，可能存在死锁风险。</p><hr/><h5 id="">方案二：逻辑过期</h5><p>设置 Key 时，不给 Redis 设置真实的过期时间，而是<strong>在 Value 中自带一个逻辑过期时间字段</strong>。<br/>实现思路如下：</p><ol start="1"><li>设置缓存时，将过期时间作为字段与数据一起存入；</li><li>查询时，先从 Redis 获取数据并判断逻辑时间是否过期；</li><li>若已过期，则<strong>异步开启一个线程进行数据刷新</strong>，同时返回旧数据给用户（保证高可用）。</li></ol><p>优点：高性能、高可用。<br/>缺点：数据可能短时间内不一致。</p><hr/><p><strong>总结：</strong></p><ul><li>若追求<strong>强一致性</strong> → 使用 <strong>分布式锁方案</strong>；</li><li>若追求<strong>高性能、高可用性</strong> → 使用 <strong>逻辑过期方案</strong>。</li></ul></div></div>]]></description><link>https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F%EF%BC%8C%E9%9B%AA%E5%B4%A9%EF%BC%8C%E5%87%BB%E7%A9%BF</link><guid isPermaLink="true">https://binzi.top/posts/%E6%95%B0%E6%8D%AE%E5%BA%93/%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F%EF%BC%8C%E9%9B%AA%E5%B4%A9%EF%BC%8C%E5%87%BB%E7%A9%BF</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 25 Oct 2025 09:13:00 GMT</pubDate></item><item><title><![CDATA[【模板】P1439 最长公共子序列]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1439%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%AD%90%E5%BA%8F%E5%88%97">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1439%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%AD%90%E5%BA%8F%E5%88%97</a></blockquote><div><h1 id="p1439-httpswwwluogucomcnproblemp1439">P1439 【模板】最长公共子序列（<a href="https://www.luogu.com.cn/problem/P1439">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>给出  的两个排列  和  ，求它们的最长公共子序列。</p><p>输入格式：</p><p>第一行是一个数 。</p><p>接下来两行，每行为  个数，为自然数  的一个排列。</p><p>输出格式：</p><p>一个数，即最长公共子序列的长度。</p><p>数据范围与说明：</p><ul><li>对于  的数据， ；</li><li>对于  的数据， 。</li></ul><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5 
3 2 1 4 5
1 2 3 4 5
</code></pre>
<p>输出：</p><pre class=""><code class="">3
</code></pre>
<h2 id="">题意</h2><p>给定  的两个排列 、，求它们的最长公共子序列（LCS）长度。</p><h2 id="">思路</h2><p>经典做法是  动态规划，但对于  的数据会超时。利用“两个序列都是排列”的性质，可以将 LCS 转化为 LIS（最长上升子序列）：</p><ol start="1"><li>建立哈希（或数组）<code>mp[val]</code> 记录值 <code>val</code> 在序列  中的位置；</li><li>遍历 ，将 $P_2[i]<code>在 $P_1$ 中的位置</code>mp[P2[i]]` 依次写入新数组；</li><li>问题就变成了：求该新数组的 LIS 长度，使用 <code>lower_bound</code> + 贪心即可达到 。</li></ol><p>复杂度：时间 ，空间 。</p><h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;

const int N = 100000 + 5;
int pos[N], b[N];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin &gt;&gt; n;
    for (int i = 1; i &lt;= n; ++i) {
        int v; cin &gt;&gt; v;
        pos[v] = i;
    }
    for (int i = 1; i &lt;= n; ++i) cin &gt;&gt; b[i];

    vector&lt;int&gt; lis;
    lis.reserve(n);
    for (int i = 1; i &lt;= n; ++i) {
        int mapped = pos[b[i]];
        auto it = lower_bound(lis.begin(), lis.end(), mapped);
        if (it == lis.end()) lis.push_back(mapped);
        else *it = mapped;
    }
    cout &lt;&lt; lis.size() &lt;&lt; &#x27;\n&#x27;;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1439%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%AD%90%E5%BA%8F%E5%88%97</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1439%E6%9C%80%E9%95%BF%E5%85%AC%E5%85%B1%E5%AD%90%E5%BA%8F%E5%88%97</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 09 May 2025 15:26:33 GMT</pubDate></item><item><title><![CDATA[【题解】P1434 [SHOI2002] 滑雪]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1434%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1434%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1434-shoi2002-httpswwwluogucomcnproblemp1434">P1434 [SHOI2002] 滑雪（<a href="https://www.luogu.com.cn/problem/P1434">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>Michael 喜欢滑雪。这并不奇怪，因为滑雪的确很刺激。可是为了获得速度，滑的区域必须向下倾斜，而且当你滑到坡底，你不得不再次走上坡或者等待升降机来载你。Michael 想知道在一个区域中最长的滑坡。区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子：</p><pre class=""><code class="">1   2   3   4   5
16  17  18  19  6
15  24  25  20  7
14  23  22  21  8
13  12  11  10  9
</code></pre>
<p>一个人可以从某个点滑向上下左右相邻四个点之一，当且仅当高度会减小。在上面的例子中，一条可行的滑坡为 $24-17-16-1$（从 $24$ 开始，在 $1$ 结束）。当然 $25$－$24$－$23$－$\ldots$－$3$－$2$－$1$ 更长。事实上，这是最长的一条。</p><p>输入格式：</p><p>输入的第一行为表示区域的二维数组的行数 $R$ 和列数 $C$。下面是 $R$ 行，每行有 $C$ 个数，代表高度（两个数字之间用 $1$ 个空格间隔）。</p><p>输出格式：</p><p>输出区域中最长滑坡的长度。</p><p>数据范围与说明：</p><p>对于 $100%$ 的数据，$1\leq R,C\leq 100$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5 5
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
</code></pre>
<p>输出：</p><pre class=""><code class="">25
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
typedef long long ll;
int a[105][105];
int dp[105][105];
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
int n, m;
bool check(int i, int j) {
    return i &gt;= 0 &amp;&amp; i &lt; n &amp;&amp; j &gt;= 0 &amp;&amp; j &lt; m;
}
int dfs(int i, int j) {
    if (dp[i][j] != 0) return dp[i][j];
    dp[i][j] = 1;
    for (int z = 0; z &lt; 4; z++) {
        int x = i + dx[z], y = j + dy[z];
        if (check(x, y) &amp;&amp; a[x][y] &lt; a[i][j]) {
            dp[i][j] = max(dp[i][j], 1 + dfs(x, y));
        }
    }
    return dp[i][j];
}
void solve() {
    cin &gt;&gt; n &gt;&gt; m;
    for (int i = 0; i &lt; n; i++) {
        for (int j = 0; j &lt; m; j++) {
            cin &gt;&gt; a[i][j];
        }
    }
    int ans = 0;
    for (int i = 0; i &lt; n; i++) {
        for (int j = 0; j &lt; m; j++) {
            ans = max(ans, dfs(i, j));
        }
    }
    cout &lt;&lt; ans &lt;&lt; &quot;\n&quot;;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    solve();
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1434%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1434%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 24 Mar 2025 10:30:14 GMT</pubDate></item><item><title><![CDATA[【深基9.例4】求第 k 小的数 - 题解]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1923%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1923%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1923-94-k-httpswwwluogucomcnproblemp1923">P1923 【深基9.例4】求第 k 小的数（<a href="https://www.luogu.com.cn/problem/P1923">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>输入 $n$（$1 \le n &lt; 5000000$ 且 $n$ 为奇数）个数字 $a<em>i$（$1 \le a</em>i &lt; {10}^9$），输出这些数字的第 $k$ 小的数。最小的数是第 $0$ 小。</p><p>请尽量不要使用 <code>nth_element</code> 来写本题，因为本题的重点在于练习分治算法。</p><p>输入格式：</p><p>第一行有两个整数，分别表示 $n$ 和 $k$。</p><p>第二行有 $n$ 个整数，第 $i$ 个数表示 $a_i$。</p><p>输出格式：</p><p>一个整数，表示第 $k$ 小的数。</p><p>数据范围与说明：</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5 1
4 3 2 1 5
</code></pre>
<p>输出：</p><pre class=""><code class="">2
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
typedef long long ll;
typedef pair&lt;int, int&gt; PLL;
int a[5000005];
int n,k;
void qsort(int i,int j){
    int l=i,r=j,mid=a[(l+r)/2];
    while(l&lt;=r){
        while(a[r]&gt;mid){
            r--;
        }
        while(a[l]&lt;mid){
            l++;
        }
        if(l&lt;=r){
            swap(a[l],a[r]);
            r--;
            l++;
        }
    }
    if(k&lt;=r){
        qsort(i,l);
    }else if(l&lt;=k){
        qsort(r,j);
    }else{
        cout&lt;&lt;a[r+1];
        return ;
    }
}
void solve() {
    cin&gt;&gt;n&gt;&gt;k;
    for(int i=0;i&lt;n;i++){
        cin&gt;&gt;a[i];
    }
    qsort(0,n-1);
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t = 1;
    while (t--) {
        solve();
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1923%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1923%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sun, 23 Mar 2025 13:41:45 GMT</pubDate></item><item><title><![CDATA[【题解】P2196]]></title><description><![CDATA[<link rel="preload" as="image" href="https://img.picui.cn/free/2025/05/15/6825a221c60ba.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2196%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2196%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p2196">P2196</h1><h2 id="">题目</h2><p>题目描述：</p><p>在一个地图上有 $N\ (N \le 20)$ 个地窖，每个地窖中埋有一定数量的地雷。同时，给出地窖之间的连接路径。当地窖及其连接的数据给出之后，某人可以从任一处开始挖地雷，然后每次可以移动到一个编号比当前节点大且联通的节点去挖地雷，当无满足条件的节点时挖地雷工作结束。设计一个挖地雷的方案，使某人能挖到最多的地雷。</p><p>输入格式：</p><p>有若干行。</p><p>第 $1$ 行只有一个数字，表示地窖的个数 $N$。</p><p>第 $2$ 行有 $N$ 个数，分别表示每个地窖中的地雷个数。</p><p>第 $3$ 行至第 $N+1$ 行表示地窖之间的连接情况：</p><p>第 $3$ 行有 $n-1$ 个数（$0$ 或 $1$），表示第一个地窖至第 $2$ 个、第 $3$ 个 $\dots$ 第 $n$ 个地窖有否路径连接。如第 $3$ 行为 $1\space 1\space 0\space 0\space 0\cdots 0$，则表示第 $1$ 个地窖至第 $2$ 个地窖有路径，至第 $3$ 个地窖有路径，至第 $4$ 个地窖、第 $5$ 个 $\dots$ 第 $n$ 个地窖没有路径。</p><p>第 $4$ 行有 $n-2$ 个数，表示第二个地窖至第 $3$ 个、第 $4$ 个 $\dots$ 第 $n$ 个地窖有否路径连接。</p><p>……</p><p>第 $n+1$ 行有 $1$ 个数，表示第 $n-1$ 个地窖至第 $n$ 个地窖有否路径连接。（为 $0$ 表示没有路径，为 $1$ 表示有路径）。</p><p>输出格式：</p><p>第一行表示挖得最多地雷时的挖地雷的顺序，各地窖序号间以一个空格分隔，不得有多余的空格。</p><p>第二行只有一个数，表示能挖到的最多地雷数。</p><p>数据范围与说明：</p><p><strong>【样例解释】</strong><br/><img src="https://img.picui.cn/free/2025/05/15/6825a221c60ba.png" height="0" width="0"/><br/>最优路径为 $1 \to 3 \to 4 \to 5$，结果为 $27$。</p><p><strong>【题目来源】</strong></p><p>NOIP 1996 提高组第三题。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5
10 8 4 7 6
1 1 1 0
0 0 0
1 1
1
</code></pre>
<p>输出：</p><pre class=""><code class="">1 3 4 5
27
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int n,ans,f[25][25],a[25],b[25];
vector&lt;int&gt; path,ans_path;
bool check(int x){
    for(int i=1;i&lt;=n;i++){
        if(f[x][i]&amp;&amp;!b[i])  return false;
    }
    return true;
}
void dfs(int x,int sum){
    if(check(x)){
        if(ans&lt;sum){
            ans=sum;
            ans_path=path;
        }
        return ;
    }
    for(int i=1;i&lt;=n;i++){
        if(f[x][i]&amp;&amp;!b[i]){
            b[i]=1;
            path.push_back(i);
            dfs(i,sum+a[i]);
            path.pop_back();
            b[i]=0;
        }
    }
    return ;
}
int main(){
    cin&gt;&gt;n;
    for(int i=1;i&lt;=n;i++){
        cin&gt;&gt;a[i];
    }
    for(int i=1;i&lt;n;i++){
        for(int j=i+1;j&lt;=n;j++){
            cin&gt;&gt;f[i][j];
        }
    }
    for(int i=1;i&lt;=n;i++){
        b[i]=1;
        path.clear();
        path.push_back(i);
        dfs(i,a[i]);
        b[i]=0;
    }
    for(int x:ans_path){
        cout&lt;&lt;x&lt;&lt;&quot; &quot;;
    }
    cout&lt;&lt;&quot;\n&quot;;
    cout&lt;&lt;ans;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2196%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2196%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 22 Mar 2025 14:27:43 GMT</pubDate></item><item><title><![CDATA[【题解】P1216 [IOI 1994 / USACO1.5] 数字三角形]]></title><description><![CDATA[<link rel="preload" as="image" href="https://cdn.luogu.com.cn/upload/image_hosting/95pzs0ne.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1216%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1216%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1216-ioi-1994--usaco15-httpswwwluogucomcnproblemp1216">P1216 [IOI 1994 / USACO1.5] 数字三角形（<a href="https://www.luogu.com.cn/problem/P1216">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>观察下面的数字金字塔。</p><p>写一个程序来查找从最高点到底部任意处结束的路径，使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。</p><p><img src="https://cdn.luogu.com.cn/upload/image_hosting/95pzs0ne.png" height="0" width="0"/></p><p>在上面的样例中，从 $7 \to 3 \to 8 \to 7 \to 5$ 的路径产生了最大权值。</p><p>输入格式：</p><p>第一个行一个正整数 $r$，表示行的数目。</p><p>后面每行为这个数字金字塔特定行包含的整数。</p><p>输出格式：</p><p>单独的一行，包含那个可能得到的最大的和。</p><p>数据范围与说明：</p><p>【数据范围】<br/>对于 $100%$ 的数据，$1\le r\le 1000$，所有输入在 $[0,100]$ 范围内。</p><p>题目翻译来自 NOCOW。</p><p>IOI1994 Day1T1 / USACO Training Section 1.5。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
</code></pre>
<p>输出：</p><pre class=""><code class="">30
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int a[1010][1010],f[1010][1010];
int main(){
int n,s=0;
cin&gt;&gt;n;
for(int i=1;i&lt;=n;i++)
for(int j=1;j&lt;=i;j++)
cin&gt;&gt;a[i][j];
for(int i=1;i&lt;=n;i++)
for(int j=1;j&lt;=i;j++)
f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];
for(int j=1;j&lt;=n;j++) s=max(s,f[n][j]);
cout&lt;&lt;s;
return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1216%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1216%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 22 Mar 2025 10:04:36 GMT</pubDate></item><item><title><![CDATA[【题解】P3654]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P3654%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P3654%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p3654">P3654</h1><h2 id="">题目</h2><p>题目描述：</p><p>可是……这个篮球场，好像很久没有使用过的样子啊……</p><p>里面堆满了学校的各种杂物呢……</p><p>我们 Aqours 的成员要怎么在里面列队站下呢？</p><p>我们浦之星女子学院的篮球场是一个 $R$ 行 $C$ 列的矩阵，其中堆满了各种学校的杂物 (用 <code>#</code> 表示)，空地 (用 <code>.</code> 表示) 好像并不多的样子呢……</p><p>我们 Aqours 现在已经一共有 $K$ 个队员了，要歌唱舞蹈起来的话，我们得排成一条 $1\times K$ 的直线，一个接一个地站在篮球场的空地上呢 (横竖均可)。</p><p>我们想知道一共有多少种可行的站位方式呢。</p><p>Aqours 的真正的粉丝的你，能帮我们算算吗？</p><p>输入格式：</p><p>第一行三个整数 $R, C, K$。</p><p>接下来的 $R$ 行 $C$ 列，表示浦之星女子学院篮球场。</p><p>输出格式：</p><p>总共的站位方式数量。</p><p>数据范围与说明：</p><table><thead><tr><th>  </th><th> $R$ </th><th> $C$ </th><th> $K$ </th><th> 备注 </th></tr></thead><tbody><tr><td> $1\sim2$ </td><td> $\leq 10$ </td><td> $\leq 10$ </td><td> $\leq \min(R,C)$ </td><td> 无 </td></tr><tr><td> $3\sim4$ </td><td> $\leq 100$ </td><td> $\leq 100$ </td><td> $\leq 1$ </td><td> 无 </td></tr><tr><td> $5\sim6$ </td><td> $\leq 100$ </td><td> $\leq 100$ </td><td> $\leq \min(R,C)$ </td><td> 没有障碍 </td></tr><tr><td> $7\sim10$ </td><td> $\leq 100$ </td><td> $\leq 100$ </td><td> $\leq \min(R,C)$ </td><td> 无 </td></tr></tbody></table><p>对于所有数据，$1 \leq R,C \leq 100$，$1 \leq k \leq \min(R,C)$。</p><h4 id="">以下是彩蛋</h4><p>在 LoveLive!Sunshine!! 动画第一季第三集中，Aqours 队长高海千歌演唱“最喜欢的话就没问题！”到副歌前时，学校因为雷击停电。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5 5 2
.###.
##.#.
..#..
#..#.
#.###
</code></pre>
<p>输出：</p><pre class=""><code class="">8
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
char a[105][105];
int r,c,k;
long long ans;
bool check(int i,int j){
    return i&gt;=0&amp;&amp;i&lt;r&amp;&amp;j&gt;=0&amp;&amp;j&lt;c;
}
void dfsr(int i,int j,int cnt){
    if(cnt==k){
        ans++;
        return ;
    }
    if(!check(i,j)) return ;
    if(a[i][j+1]==&#x27;.&#x27;){
        dfsr(i,j+1,cnt+1);
    }
    return ;
}
void dfsu(int i,int j,int cnt){
    if(cnt==k){
        ans++;
        return ;
    }
    if(!check(i,j)) return ;
    if(a[i+1][j]==&#x27;.&#x27;){
        dfsu(i+1,j,cnt+1);
    }
    return ;
}
int main(){
    cin&gt;&gt;r&gt;&gt;c&gt;&gt;k;
    for(int i=0;i&lt;r;i++){
        for(int j=0;j&lt;c;j++){
            cin&gt;&gt;a[i][j];
        }
    }
    for(int i=0;i&lt;r;i++){
        for(int j=0;j&lt;c;j++){
            if(a[i][j]==&#x27;.&#x27;){
                dfsr(i,j,1);
                dfsu(i,j,1);
            }
        }
    }
    if(k==1){
        cout&lt;&lt;ans/2;
    }else{
        cout&lt;&lt;ans;
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P3654%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P3654%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 19 Mar 2025 14:03:00 GMT</pubDate></item><item><title><![CDATA[【题解】P10415]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10415%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10415%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p10415httpswwwluogucomcnproblemp10415">P10415（<a href="https://www.luogu.com.cn/problem/P10415">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>给定一个 $W\times H$ 的长方形，两边长度均为整数。小蓝想把它切割为很多个边长为整数的小正方形。假设切割没有任何损耗，正方形的边长至少为 $2$，不允许出现余料，要求所有正方形的大小相等，请问最多能切割出多少个?</p><p>输入格式：</p><p>输入一行，包含两个整数 $W, H$，用一个空格分隔。</p><p>输出格式：</p><p>输出一行包含一个整数表示答案。如果不存在满足要求的方案，输出 $0$。</p><p>数据范围与说明：</p><p><strong>【样例解释 1】</strong></p><p>切割成 $5\times 10=50$ 个边长为 $2$ 的正方形。</p><p><strong>【评测用例规模与约定】</strong></p><p>对于 $30%$ 的评测用例，$1\le W,H\le 1000$；<br/>对于 $60%$ 的评测用例，$1\le W,H\le 10^6$；<br/>对于所有评测用例，$1\le W,H\le 10^9$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">10 20
</code></pre>
<p>输出：</p><pre class=""><code class="">50
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">6 9
</code></pre>
<p>输出：</p><pre class=""><code class="">6
</code></pre>
<p>输入输出样例 #3</p><p>输入：</p><pre class=""><code class="">8 13
</code></pre>
<p>输出：</p><pre class=""><code class="">0
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>给定一个 $W\times H$ 的长方形，两边长度均为整数。小蓝想把它切割为很多个边长为整数的小正方形。假设切割没有任何损耗，正方形的边长至少为 $2$，不允许出现余料，要求所有正方形的大小相等，请问最多能切割出多少个?</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
const int N=1e6+10;
typedef long long ll;
typedef pair&lt;int, int&gt; PLL;
int gcd(int a,int b){
    return b? gcd(b,a%b):a;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t = 1;
    // cin &gt;&gt; t;
    ll n,m;
   cin&gt;&gt;n&gt;&gt;m;
    int w=gcd(n,m);//最大正方形的边长
    if(w==1){//如果两个都是质数那么不能完全分解
        cout&lt;&lt;0;
        return 0;
    }
    for(int i=2;i*i&lt;=w;i++){//最大公约数便是最大正方形的面积，因此用i*i来比较 
        if(n%i==0&amp;&amp;m%i==0){
            cout&lt;&lt;n/i*m/i;
            return 0;
        }
    }
    cout&lt;&lt;n/w*m/w;//特判w是质数的情况
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10415%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10415%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 19 Mar 2025 06:26:43 GMT</pubDate></item><item><title><![CDATA[【题解】P1048 [NOIP 2005 普及组] 采药]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1048%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1048%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1048-noip-2005--httpswwwluogucomcnproblemp1048">P1048 [NOIP 2005 普及组] 采药（<a href="https://www.luogu.com.cn/problem/P1048">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>辰辰是个天资聪颖的孩子，他的梦想是成为世界上最伟大的医师。为此，他想拜附近最有威望的医师为师。医师为了判断他的资质，给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说：“孩子，这个山洞里有一些不同的草药，采每一株都需要一些时间，每一株也有它自身的价值。我会给你一段时间，在这段时间里，你可以采到一些草药。如果你是一个聪明的孩子，你应该可以让采到的草药的总价值最大。”</p><p>如果你是辰辰，你能完成这个任务吗？</p><p>输入格式：</p><p>第一行有 $2$ 个整数 $T$（$1 \le T \le 1000$）和 $M$（$1 \le  M \le 100$），用一个空格隔开，$T$ 代表总共能够用来采药的时间，$M$ 代表山洞里的草药的数目。</p><p>接下来的 $M$ 行每行包括两个在 $1$ 到 $100$ 之间（包括 $1$ 和 $100$）的整数，分别表示采摘某株草药的时间和这株草药的价值。</p><p>输出格式：</p><p>输出在规定的时间内可以采到的草药的最大总价值。</p><p>数据范围与说明：</p><p><strong>【数据范围】</strong></p><ul><li>对于 $30%$ 的数据，$M \le 10$；</li><li>对于全部的数据，$M \le 100$。</li></ul><p><strong>【题目来源】</strong></p><p>NOIP 2005 普及组第三题</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">70 3
71 100
69 1
1 2
</code></pre>
<p>输出：</p><pre class=""><code class="">3
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>辰辰是个天资聪颖的孩子，他的梦想是成为世界上最伟大的医师。为此，他想拜附近最有威望的医师为师。医师为了判断他的资质，给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说：“孩子，这个山洞里有一些不同的草药，采每一株都需要一些时间，每一株也有它自身的价值。我会给你一段时间，在这段时间里，你可以采到一些草药。如果你是一个聪明的孩子，你应该可以让采到的草药的总价值最大。”</p><p>如果你是辰辰，你能完成这个任务吗？</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;

typedef long long ll;
typedef pair&lt;int, int&gt; PLL;
int m,v[1005],w[1005];
int dp[1005][1005];
int t = 1;
void solve() {
    cin &gt;&gt; t&gt;&gt;m;
    for(int i=1;i&lt;=m;i++){
        cin&gt;&gt;v[i]&gt;&gt;w[i];
    }
    for(int i=1;i&lt;=m;i++){
        for(int j=t;j&gt;0;j--){
            if(j&lt;v[i]){
                dp[i][j]=dp[i-1][j];
            }else{
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
            }
        }
    }
    cout&lt;&lt;dp[m][t];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    
    solve();
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1048%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1048%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 18 Mar 2025 14:41:33 GMT</pubDate></item><item><title><![CDATA[【题解】P2347]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2347%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2347%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p2347httpswwwluogucomcnproblemp2347">P2347（<a href="https://www.luogu.com.cn/problem/P2347">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>设有 $1\mathrm{g}$、$2\mathrm{g}$、$3\mathrm{g}$、$5\mathrm{g}$、$10\mathrm{g}$、$20\mathrm{g}$ 的砝码各若干枚（其总重 ），可以表示成多少种重量？</p><p>输入格式：</p><p>输入方式：$a<em>1 , a</em>2  ,a<em>3 , a</em>4 , a<em>5 ,a</em>6$</p><p>（表示 $1\mathrm{g}$ 砝码有 $a<em>1$ 个，$2\mathrm{g}$ 砝码有 $a</em>2$ 个，$\dots$，$20\mathrm{g}$ 砝码有 $a_6$ 个）</p><p>输出格式：</p><p>输出方式：<code>Total=N</code></p><p>（$N$ 表示用这些砝码能称出的不同重量的个数，但不包括一个砝码也不用的情况）</p><p>数据范围与说明：</p><p><strong>【题目来源】</strong></p><p>NOIP 1996 提高组第四题</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">1 1 0 0 0 0
</code></pre>
<p>输出：</p><pre class=""><code class="">Total=3
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int a[]={1,2,3,5,10,20};
int b[300];
int dp[1100];
int main(){
    int sum=0;
    for(int i=0;i&lt;6;i++){
        cin&gt;&gt;b[i];
        sum+=a[i]*b[i];
    }
    int ans=0;
    dp[0] = 1;
    for(int i=0;i&lt;6;i++){
        for(int j=sum;j&gt;=0;j--){
            for (int k = 1; k &lt;= b[i]; k++){ 
if (dp[j - a[i]*k] == 1 &amp;&amp; j - a[i]*k &gt;= 0 &amp;&amp; dp[j] == 0){
                    dp[j] = 1,ans++;
                }
}
        }
    }
    cout&lt;&lt;&quot;Total=&quot;&lt;&lt;ans;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2347%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2347%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 18 Mar 2025 14:31:37 GMT</pubDate></item><item><title><![CDATA[【题解】P8195]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P8195%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P8195%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p8195">P8195</h1><h2 id="">题目</h2><p>题目描述：</p><p>传智专修学院给了小智一个仅包含小写字母的字符串 $s$，他想知道，里面出现了多少次子串 <code>chuanzhi</code> 呢。</p><p>我们称一个字符串 $t$ 是 $s$ 的子串，当且仅当将 $s$ 的开头若干个（可以为 0 个）连续字符和结尾若干个（可以为 0 个）连续字符删去后，剩下的字符串和 $t$ 相同。例如，我们称 <code>ab</code> 是 <code>abc</code> 的子串，但 <code>ac</code> 不是 <code>abc</code> 的子串。</p><p>输入格式：</p><p>输入只有一行一个字符串，表示字符串 $s$。</p><p>输出格式：</p><p>输出一行一个整数表示答案。</p><p>数据范围与说明：</p><h3 id="">数据规模与约定</h3><p>对于全部的测试点，保证 $1 \leq |s| \leq 4 \times 10^5$，$|s|$ 表示 $s$ 的长度，且 $s$ 中只有小写字母。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">welcometochuanzhicupchuanzhi
</code></pre>
<p>输出：</p><pre class=""><code class="">2
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int main() {
    string s;
    cin &gt;&gt; s;
    string target = &quot;chuanzhi&quot;;
    int index = 0, count = 0;
    for (char c : s) {
        if (c == target[index]) {
            index++;
            if (index == 8) {
                count++;
                index = 0;
            }
        } else {
            index = (c == target[0]) ? 1 : 0;
        }
    }
    cout &lt;&lt; count;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P8195%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P8195%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 15 Mar 2025 08:31:26 GMT</pubDate></item><item><title><![CDATA[【题解】P8196]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P8196%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P8196%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p8196httpswwwluogucomcnproblemp8196">P8196（<a href="https://www.luogu.com.cn/problem/P8196">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>给定一个长度为 $n$ 的数列 $a$，对于一个有序整数三元组 $(i, j, k)$，若其满足 $1 \leq i \leq j \leq k \leq n$ 并且 $a<em>i +a</em>j  = a_k$，则我们称这个三元组是「传智的」。</p><p>现在请你计算，有多少有序整数三元组是传智的。</p><p>输入格式：</p><p><strong>本题单测试点内有多组测试数据</strong>。</p><p>输入的第一行是一个整数 $T$，表示数据组数。</p><p>对于每组数据：</p><p>第一行是一个整数，表示数列的长度 $n$。<br/>第二行有 $n$ 个整数，第 $i$ 个整数表示 $a_i$。</p><p>输出格式：</p><p>对于每组数据，输出一行一个整数表示答案。</p><p>数据范围与说明：</p><h3 id="-1-">样例 1 解释</h3><p>对于第一组数据，因为 $a<em>1 + a</em>1 = a<em>2$，$a</em>1 + a<em>2 = a</em>3$，故共 $(1, 1, 2)$ 和 $(1, 2, 3)$ 两个三元组。<br/>对于第二组数据六个三元组分别是：</p><ul><li>$(1, 1, 2)$</li><li>$(1, 2, 3)$</li><li>$(1, 3, 4)$</li><li>$(1, 4, 5)$</li><li>$(2, 2, 4)$</li><li>$(2, 3, 5)$</li></ul><h3 id="">数据规模与约定</h3><p>对于全部测试点，保证 $1 \leq T \leq 100$，$1 \leq n , a_i \leq 100$，且各个测试点的 $n$ 之和不超过 $100$，即 $\sum n \leq 100$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">2
3
1 2 3
5
1 2 3 4 5
</code></pre>
<p>输出：</p><pre class=""><code class="">2
6
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
typedef long long ll;
typedef pair&lt;int, int&gt; PLL;
int a[105];
void solve() {
    int n,cnt=0;
    cin&gt;&gt;n;
    for(int i=1;i&lt;=n;i++){
        cin&gt;&gt;a[i];
    }
    for(int i=1;i&lt;=n;i++){
        for(int j=i;j&lt;=n;j++){
            for(int k=j;k&lt;=n;k++){
                if(a[i]+a[j]==a[k]){
                    cnt++;
                }
            }
        }
    }
    cout&lt;&lt;cnt&lt;&lt;endl;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t = 1;
    cin &gt;&gt; t;
    while (t--) {
        solve();
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P8196%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P8196%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 15 Mar 2025 08:06:58 GMT</pubDate></item><item><title><![CDATA[口算练习题 - 题解]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1957%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1957%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="httpswwwluogucomcnproblemp1957">口算练习题（<a href="https://www.luogu.com.cn/problem/P1957">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>王老师正在教简单算术运算。细心的王老师收集了 $i$ 道学生经常做错的口算题，并且想整理编写成一份练习。编排这些题目是一件繁琐的事情，为此他想用计算机程序来提高工作效率。王老师希望尽量减少输入的工作量，比如 $\texttt{5+8}$ 的算式最好只要输入 $\texttt 5$ 和 $\texttt 8$，输出的结果要尽量详细以方便后期排版的使用，比如对于上述输入进行处理后输出 $\texttt{5+8=13}$ 以及该算式的总长度 $6$。王老师把这个光荣的任务交给你，请你帮他编程实现以上功能。</p><p>输入格式：</p><p>第一行一个整数 $i$。</p><p>接着的 $i$ 行为需要输入的算式，每行可能有三个数据或两个数据。</p><p>若该行为三个数据则第一个数据表示运算类型，$\texttt a$ 表示加法运算，$\texttt b$ 表示减法运算，$\texttt c$ 表示乘法运算，接着的两个数据表示参加运算的运算数。</p><p>若该行为两个数据，则表示本题的运算类型与上一题的运算类型相同，而这两个数据为运算数。</p><p>输出格式：</p><p>输出 $2\times i$ 行。对于每个输入的算式，输出完整的运算式及结果，第二行输出该运算式的总长度。</p><p>数据范围与说明：</p><h3 id="">【数据规模与约定】</h3><p>对于 $50%$ 的数据，输入的算式都有三个数据。</p><p>对于所有数据，$0&lt;i\leq 50$，第一个算式一定有三个数据，运算数为非负整数且小于 $10000$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">4
a 64 46
275 125
c 11 99
b 46 64
</code></pre>
<p>输出：</p><pre class=""><code class="">64+46=110
9
275+125=400
11
11*99=1089
10
46-64=-18
9
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int main(){
    char c;
    int n,a,b;
    cin&gt;&gt;n;
    string s;
    for(int i=0;i&lt;n;i++){
        cin&gt;&gt;s;
        if(s[0]&gt;=&#x27;a&#x27;&amp;&amp;s[0]&lt;=&#x27;c&#x27;){
            c=s[0];
            cin&gt;&gt;a&gt;&gt;b;
        }else{
            a=stoi(s);
            cin&gt;&gt;b;
        }
       if(c==&#x27;a&#x27;){
           string ans=to_string(a)+&quot;+&quot;+to_string(b)+&quot;=&quot;+to_string(a+b);
           cout&lt;&lt;ans&lt;&lt;endl;
           cout&lt;&lt;ans.size()&lt;&lt;endl;
       }else if(c==&#x27;b&#x27;){
           string ans=to_string(a)+&quot;-&quot;+to_string(b)+&quot;=&quot;+to_string(a-b);
           cout&lt;&lt;ans&lt;&lt;endl;
           cout&lt;&lt;ans.size()&lt;&lt;endl;
       }else if(c==&#x27;c&#x27;){
           string ans=to_string(a)+&quot;*&quot;+to_string(b)+&quot;=&quot;+to_string(a*b);
           cout&lt;&lt;ans&lt;&lt;endl;
           cout&lt;&lt;ans.size()&lt;&lt;endl;
       }
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1957%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1957%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Thu, 13 Mar 2025 08:56:10 GMT</pubDate></item><item><title><![CDATA[【题解】P10900]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10900%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10900%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p10900httpswwwluogucomcnproblemp10900">P10900（<a href="https://www.luogu.com.cn/problem/P10900">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>在诗人的眼中，数字是生活的韵律，也是诗意的表达。</p><p>小蓝，当代顶级诗人与数学家，被赋予了“数学诗人” 的美誉。他擅长将冰冷的数字与抽象的诗意相融合，并用优雅的文字将数学之美展现于纸上。</p><p>某日，小蓝静坐书桌前，目光所及，展现着 $n$ 个数字，它们依次为 $a<em>1, a</em>2, \cdots, a_n$，熠熠生辉。小蓝悟到，如果一个数能够以若干个（至少两个）<strong>连续的正整数</strong>相加表示，那么它就蕴含诗意。例如，数字 $6$ 就蕴含诗意，因为<br/>它可以表示为 $1 + 2 + 3$。而 $8$ 则缺乏诗意，因为它无法用连续的正整数相加表示。</p><p>小蓝希望他面前的所有数字都蕴含诗意，为此，他决定从这 $n$ 个数字中删除一部分。请问，小蓝需要删除多少个数字，才能使剩下的数字全部蕴含诗意？</p><p>输入格式：</p><p>输入的第一行包含一个整数 $n$，表示展示的数字个数。</p><p>第二行包含 $n$ 个整数 $a<em>1, a</em>2, \cdots, a_n$，相邻整数之间使用一个空格分隔，表示展示的数字。</p><p>输出格式：</p><p>输出一行包含一个整数，表示小蓝需要删除的数字个数，以使剩下的数字全部蕴含诗意。</p><p>数据范围与说明：</p><p><strong>【样例说明】</strong></p><p>在样例中，数字 $3$ 可以表示为 $1 + 2$，数字 $6$ 可以表示为 $1 + 2 + 3$，数字 $8$ 无法表示为连续的正整数相加，因此，需要删除的数字个数为 $1$。</p><p><strong>【评测用例规模与约定】</strong></p><p>对于 $30%$ 的评测用例，$1 \le n \le 10^3$，$1 \le a<em>i \le 10^3$。<br/>对于所有评测用例，$1 \le n \le 2 \times 10^5$，$1 \le a</em>i \le 10^{16}$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">3
3 6 8
</code></pre>
<p>输出：</p><pre class=""><code class="">1
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>在诗人的眼中，数字是生活的韵律，也是诗意的表达。</p><p>小蓝，当代顶级诗人与数学家，被赋予了“数学诗人” 的美誉。他擅长将冰冷的数字与抽象的诗意相融合，并用优雅的文字将数学之美展现于纸上。</p><p>某日，小蓝静坐书桌前，目光所及，展现着 $n$ 个数字，它们依次为 $a<em>1, a</em>2, \cdots, a_n$，熠熠生辉。小蓝悟到，如果一个数能够以若干个（至少两个）<strong>连续的正整数</strong>相加表示，那么它就蕴含诗意。例如，数字 $6$ 就蕴含诗意，因为<br/>它可以表示为 $1 + 2 + 3$。而 $8$ 则缺乏诗意，因为它无法用连续的正整数相加表示。</p><p>小蓝希望他面前的所有数字都蕴含诗意，为此，他决定从这 $n$ 个数字中删除一部分。请问，小蓝需要删除多少个数字，才能使剩下的数字全部蕴含诗意？</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
typedef long long ll;
const int N=2e5+10;
ll a[N];
bool check(ll n){
    return n&amp;(n-1);
}

int main(){
    ll n;
    cin&gt;&gt;n;
    for(int i=0;i&lt;n;i++) cin&gt;&gt;a[i];
    ll ans=0;
    for(int i=0;i&lt;n;i++){
        if(!check(a[i])){
            ans++;
        }
    }
    cout&lt;&lt;ans;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10900%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10900%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Thu, 13 Mar 2025 07:04:51 GMT</pubDate></item><item><title><![CDATA[【题解】P5638]]></title><description><![CDATA[<link rel="preload" as="image" href="https://cdn.luogu.com.cn/upload/image_hosting/42z5mohh.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5638%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5638%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p5638">P5638</h1><h2 id="">题目</h2><p>题目描述：</p><p>小 K 打下的江山一共有 $n$ 个城市，城市 $i$ 和城市 $i+1$ 有一条双向高速公路连接，走这条路要耗费时间 $a_i$。</p><p>小 K 为了关心人民生活，决定定期进行走访。他每一次会从 $1$ 号城市到 $n$ 号城市并在经过的城市进行访问。其中终点必须为城市 $n$。</p><p>不仅如此，他还有一个传送器，传送半径为 $k$，也就是可以传送到 $i-k$ 和 $i+k$。如果目标城市编号小于 $1$ 则为 $1$，大于 $n$ 则为 $n$。</p><p>但是他的传送器电量不足，只能传送一次，况且由于一些原因，他想尽量快的完成访问，于是就想问交通部部长您最快的时间是多少。</p><p>注意：<strong>他可以不访问所有的城市，使用传送器不耗费时间</strong>。</p><p>输入格式：</p><p>两行，第一行两个正整数 $n,k$。</p><p>第二行 $n-1$ 个整数，第 $i$ 个表示 $a_i$。</p><p>输出格式：</p><p>一个整数，表示答案。</p><p>数据范围与说明：</p><h3 id="-1">样例解释 1：</h3><p>样例 1,2 的图示均为以下图片：</p><p><img src="https://cdn.luogu.com.cn/upload/image_hosting/42z5mohh.png" height="0" width="0"/></p><p>不使用传送器直接走，答案为 $6$，可以证明这个是最小值。</p><h3 id="-2">样例解释 2：</h3><p>在 $3$ 处使用，传送到 $4$，答案为 $3$，可以证明这个是最小值。</p><h3 id="">数据范围：</h3><p>对于所有数据，$n\ge 2$，$k\ge 1$，$a_i &gt; 0$。</p><table><thead><tr><th> 测试点编号 </th><th> $n$ 的范围 </th><th> $k$ 的范围 </th><th> $a_i$ 的范围 </th></tr></thead><tbody><tr><td> $1\sim 10$ </td><td> $\le 100$ </td><td> $\le 100$ </td><td> $\le 10^5$ </td></tr><tr><td> $11\sim 20$ </td><td> $\le 3\times 10^3$ </td><td> $\le 3\times 10^3$ </td><td> $\le 10^9$ </td></tr><tr><td> $21$ </td><td> $\le 10^5$ </td><td> $\le 10^5$ </td><td> $\le 10^{12}$ </td></tr><tr><td> $22$ </td><td> $\le 2\times 10^5$ </td><td> $\le 2\times 10^5$ </td><td> $\le 10^{12}$ </td></tr><tr><td> $23$ </td><td> $\le 5\times 10^5$ </td><td> $\le 5\times 10^5$ </td><td> $\le 10^{12}$ </td></tr><tr><td> $24\sim 25$ </td><td> $\le 10^6$ </td><td> $\le 10^6$ </td><td> $\le 10^{12}$ </td></tr></tbody></table><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">4 0
1 2 3
</code></pre>
<p>输出：</p><pre class=""><code class="">6
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">4 1
1 2 3
</code></pre>
<p>输出：</p><pre class=""><code class="">3
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
typedef long long ll;
const int N=1e6+10;
ll a[N],s[N];
int main(){
    ll n,k;
    cin&gt;&gt;n&gt;&gt;k;
    if(k+1&gt;=n){
        cout&lt;&lt;0;
        return 0;
    }
    for(int i=1;i&lt;=n-1;i++){
        cin&gt;&gt;a[i];
        s[i]=s[i-1]+a[i];
    }
    ll ans=LLONG_MAX;
    for(int i=1;i&lt;=n-k;i++){
        ll sum = s[i + k - 1] - s[i - 1];
        ans=min(ans,s[n-1]-sum);
    }
    cout&lt;&lt;ans;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5638%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5638%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 12 Mar 2025 17:55:35 GMT</pubDate></item><item><title><![CDATA[【题解】P1143 进制转换]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1143%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1143%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1143-httpswwwluogucomcnproblemp1143">P1143 进制转换（<a href="https://www.luogu.com.cn/problem/P1143">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>请你编一程序实现两种不同进制之间的数据转换。</p><p>输入格式：</p><p>共三行，第一行是一个正整数，表示需要转换的数的进制 $n\ (2\le n\le 16)$，第二行是一个 $n$ 进制数，若 $n&gt;10$ 则用大写字母 $\verb!A!\sim \verb!F!$ 表示数码 $10\sim 15$，并且该 $n$ 进制数对应的十进制的值不超过 $10^9$，第三行也是一个正整数，表示转换之后的数的进制 $m\ (2\le m\le 16)$。</p><p>输出格式：</p><p>一个正整数，表示转换之后的 $m$ 进制数。</p><p>数据范围与说明：</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">16
FF
2
</code></pre>
<p>输出：</p><pre class=""><code class="">11111111
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
int main(){
    int n,m,num=0;
    string s;
    cin&gt;&gt;n;
    cin&gt;&gt;s;
    cin&gt;&gt;m;
    reverse(s.begin(),s.end());
    for(int i=0;i&lt;s.size();i++){
        if(s[i]&gt;=&#x27;A&#x27;&amp;&amp;s[i]&lt;=&#x27;F&#x27;){
            num+=(s[i]-55)*pow(n,i);
        }else{
            num+=(s[i]-&#x27;0&#x27;)*pow(n,i);
        }
    }
    string ans;
    while(num){
        int tmp=num%m;
        if(tmp&gt;10){
            ans.push_back(tmp+55);
        }else{
            ans.push_back(tmp+&#x27;0&#x27;);
        }
        num/=m;
    }
    reverse(ans.begin(),ans.end());
    cout&lt;&lt;ans;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1143%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1143%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sun, 09 Mar 2025 11:13:37 GMT</pubDate></item><item><title><![CDATA[【题解】P1225]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1225%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1225%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1225">P1225</h1><h2 id="">题目</h2><p>题目描述：</p><p>黑白棋游戏的棋盘由 $4 \times 4$ 方格阵列构成。棋盘的每一方格中放有 $1$ 枚棋子，共有 $8$ 枚白棋子和 $8$ 枚黑棋子。这 $16$ 枚棋子的每一种放置方案都构成一个游戏状态。在棋盘上拥有 $1$ 条公共边的 $2$ 个方格称为相邻方格。一个方格最多可有 $4$ 个相邻方格。在玩黑白棋游戏时，每一步可将任何 $2$ 个相邻方格中棋子互换位置。对于给定的初始游戏状态和目标游戏状态，编程计算从初始游戏状态变化到目标游戏状态的最短着棋序列。</p><p>输入格式：</p><p>输入文件共有 $8$ 行。前四行是初始游戏状态，后四行是目标游戏状态。每行 $4$ 个数分别表示该行放置的棋子颜色。“ $0$ ”表示白棋；“ $1$ ”表示黑棋。</p><p>输出格式：</p><p>输出文件的第一行是着棋步数 $n$。接下来 $n$ 行，每行 $4$ 个数分别表示该步交换棋子的两个相邻方格的位置。例如，abcd 表示将棋盘上 $(a,b)$ 处的棋子与 $(c,d)$ 处的棋子换位。</p><p>数据范围与说明：</p><p>由 @zhouyonglong 提供 SPJ</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">1111
0000
1110
0010
1010
0101
1010
0101
</code></pre>
<p>输出：</p><pre class=""><code class="">4
1222
1424
3242
4344
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int f[5005][5005],len=1;
void func(int x){
    for(int i=1;i&lt;=len;i++){
        f[x][i]=f[x-1][i]+f[x-2][i];
    }
    for(int i=1;i&lt;=len;i++){
        if(f[x][i]&gt;=10){
            f[x][i+1]+=f[x][i]/10;
            f[x][i]%=10;
            while(f[x][len+1]){
                len++;
            }
        }
    }
}
int main(){
    int n;
    cin&gt;&gt;n;
    f[1][1]=1;
    f[2][1]=2;
    for(int i=3;i&lt;=n;i++){
        func(i);
    }
    for(int i=len;i&gt;=1;i--){
        cout&lt;&lt;f[n][i];
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1225%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1225%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Thu, 06 Mar 2025 12:51:34 GMT</pubDate></item><item><title><![CDATA[【题解】P1304 哥德巴赫猜想]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1304%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1304%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1304-httpswwwluogucomcnproblemp1304">P1304 哥德巴赫猜想（<a href="https://www.luogu.com.cn/problem/P1304">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>输入一个偶数 $N$，验证 $4\sim N$ 所有偶数是否符合哥德巴赫猜想：任一大于 $2$ 的偶数都可写成两个质数之和。如果一个数不止一种分法，则输出第一个加数相比其他分法最小的方案。例如 $10$，$10=3+7=5+5$，则 $10=5+5$ 是错误答案。</p><p>输入格式：</p><p>第一行输入一个正偶数 $N$</p><p>输出格式：</p><p>输出 $\dfrac{N-2}{2}$ 行。对于第 $i$ 行：</p><p>首先先输出正偶数 $2i+2$，然后输出等号，再输出加和为 $2i+2$ 且第一个加数最小的两个质数，以加号隔开。</p><p>数据范围与说明：</p><p>数据保证，。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">10
</code></pre>
<p>输出：</p><pre class=""><code class="">4=2+2
6=3+3
8=3+5
10=3+7
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
bool prime(int x){
    if(x&lt;=1)  return false;
    for(int i=2;i&lt;=sqrt(x);i++){
        if(x%i==0) return false;
    }
    return true;
}
int main(){
    int n;
    cin&gt;&gt;n;
    for(int i=4;i&lt;=n;i+=2){
        for(int j=2;j&lt;i;j++){
            if(prime(j)&amp;&amp;prime(i-j)){
                cout&lt;&lt;i&lt;&lt;&quot;=&quot;&lt;&lt;j&lt;&lt;&quot;+&quot;&lt;&lt;i-j&lt;&lt;endl;
                break;
            }
        }
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1304%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1304%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Thu, 06 Mar 2025 07:56:13 GMT</pubDate></item><item><title><![CDATA[【题解】P1217 [USACO1.5] 回文质数]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1217%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1217%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1217-usaco15-httpswwwluogucomcnproblemp1217">P1217 [USACO1.5] 回文质数（<a href="https://www.luogu.com.cn/problem/P1217">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>因为 $151$ 既是一个质数又是一个回文数（从左到右和从右到左是看一样的），所以 $151$ 是回文质数。</p><p>写一个程序来找出范围 $[a,b] (5 \le a &lt; b \le 100,000,000)$（一亿）间的所有回文质数。</p><p>输入格式：</p><p>第一行输入两个正整数 $a$ 和 $b$。</p><p>输出格式：</p><p>输出一个回文质数的列表，一行一个。</p><p>数据范围与说明：</p><p>Hint 1: Generate the palindromes and see if they are prime.</p><p>提示 1: 找出所有的回文数再判断它们是不是质数（素数）.</p><p>Hint 2: Generate palindromes by combining digits properly. You might need more than one of the loops like below.</p><p>提示 2: 要产生正确的回文数，你可能需要几个像下面这样的循环。</p><p>题目翻译来自NOCOW。</p><p>USACO Training Section 1.5</p><p>产生长度为 $5$ 的回文数：</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">for (d1 = 1; d1 &lt;= 9; d1+=2) {    // 只有奇数才会是素数
     for (d2 = 0; d2 &lt;= 9; d2++) {
         for (d3 = 0; d3 &lt;= 9; d3++) {
           palindrome = 10000*d1 + 1000*d2 +100*d3 + 10*d2 + d1;//(处理回文数...)
         }
     }
 }
</code></pre>
<p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5 500
</code></pre>
<p>输出：</p><pre class=""><code class="">5
7
11
101
131
151
181
191
313
353
373
383
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int n,m;
bool prime(int x) {
    if (x &lt; 2) return false;
    if (x == 2) return true;
    if (x % 2 == 0) return false;
    for (int i = 3; i * i &lt;= x; i += 2) {
        if (x % i == 0) return false;
    }
    return true;
}
bool check(int x){
    string ans;
    int s=x;
    while(s){
        ans.push_back(s%10+&#x27;0&#x27;);
        s/=10;
    }
    int l=0,r=ans.size()-1;
    while(l&lt;r){
        if(ans[l++]!=ans[r--]){
            return false;
        }
    }
    return true;
}
int main(){
    cin&gt;&gt;n&gt;&gt;m;
    if(m&gt;=9999999) m=9999999;
if(n%2==0)n++;
    for(int i=n;i&lt;=m;i++){
        if(check(i)&amp;&amp;prime(i)){
            cout&lt;&lt;i&lt;&lt;endl;
        }
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1217%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1217%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 05 Mar 2025 16:13:44 GMT</pubDate></item><item><title><![CDATA[【题解】P2543]]></title><description><![CDATA[<link rel="preload" as="image" href="https://cdn.luogu.com.cn/upload/pic/1654.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2543%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2543%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p2543">P2543</h1><h2 id="">题目</h2><p>题目描述：</p><p><img src="https://cdn.luogu.com.cn/upload/pic/1654.png" height="0" width="0"/></p><p>输入格式：</p><p>输入文件中包含两个字符串X和Y。当中两字符串非0即1。序列长度均小于9999。</p><p>输出格式：</p><p>X和Y的最长公共子序列长度。</p><p>数据范围与说明：</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">01010101010 00000011111
</code></pre>
<p>输出：</p><pre class=""><code class="">6
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">01011 010010101111111111
</code></pre>
<p>输出：</p><pre class=""><code class="">5
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int dp[10001][10001];
int main(){
    string s1;
    string s2;
    int cnt=0;
    cin&gt;&gt;s1&gt;&gt;s2;
    for(int i=1;i&lt;=s1.size();i++){
        for(int j=1;j&lt;=s2.size();j++){
            if(s1[i-1]==s2[j-1])    dp[i][j]=dp[i-1][j-1]+1;
            else    dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
        }
    }
    cout&lt;&lt;dp[s1.size()][s2.size()];
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2543%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2543%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 05 Mar 2025 09:11:16 GMT</pubDate></item><item><title><![CDATA[【题解】P10898]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10898%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10898%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p10898httpswwwluogucomcnproblemp10898">P10898（<a href="https://www.luogu.com.cn/problem/P10898">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>小蓝正在玩拼图游戏，他有 $7385137888721$ 个 $2 \times 2$ 的方块和 $10470245$ 个 $1 \times 1$ 的方块，他需要从中挑出一些来拼出一个正方形，比如用 $3$ 个 $2 \times 2$ 和 $4$ 个 $1 \times 1$ 的方块可以拼出一个 $4 \times 4$ 的正方形，用 $9$ 个 $2 \times 2$ 的方块可以拼出一个 $6 \times 6$ 的正方形，请问小蓝能拼成的最大的正方形的边长为多少。</p><p>输入格式：</p><p>这是一道结果填空的题，你只需要算出结果后提交即可。本题的结果为一个整数，在提交答案时只填写这个整数，填写多余的内容将无法得分。</p><p>输出格式：</p><p>这是一道结果填空的题，你只需要算出结果后提交即可。本题的结果为一个整数，在提交答案时只填写这个整数，填写多余的内容将无法得分。</p><p>数据范围与说明：</p><h2 id="">题意</h2><p>简述：</p><p>小蓝正在玩拼图游戏，他有 $7385137888721$ 个 $2 \times 2$ 的方块和 $10470245$ 个 $1 \times 1$ 的方块，他需要从中挑出一些来拼出一个正方形，比如用 $3$ 个 $2 \times 2$ 和 $4$ 个 $1 \times 1$ 的方块可以拼出一个 $4 \times 4$ 的正方形，用 $9$ 个 $2 \times 2$ 的方块可以拼出一个 $6 \times 6$ 的正方形，请问小蓝能拼成的最大的正方形的边长为多少。</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int main(){
    long long t=7385137888721;
    long long o=10470245,n=0;
    for(long long i=o;i&gt;=1;i--){
        if(i%4==0){
            n=i/4;
            break;
        }
    }
    t+=n;
    t=sqrt(t*4);
    cout&lt;&lt;t;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10898%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10898%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 03 Mar 2025 09:22:29 GMT</pubDate></item><item><title><![CDATA[【题解】P10426]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10426%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10426%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p10426httpswwwluogucomcnproblemp10426">P10426（<a href="https://www.luogu.com.cn/problem/P10426">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>在一个神秘的森林里，住着一个小精灵名叫小蓝。有一天，他偶然发现了一个隐藏在树洞里的宝藏，里面装满了闪烁着美丽光芒的宝石。这些宝石都有着不同的颜色和形状，但最引人注目的是它们各自独特的 “闪亮度” 属性。每颗宝石都有一个与生俱来的特殊能力，可以发出不同强度的闪光。小蓝共找到了 $n$ 枚宝石，第 $i$ 枚宝石的 “闪亮度” 属性值为 $H_i$，小蓝将会从这 $n$ 枚宝石中选出三枚进行组合，组合之后的精美程度 $S$ 可以用以下公式来衡量：</p><p>$$S = H<em>a H</em>b H<em>c \cdot \frac{\operatorname{LCM}(H</em>a, H<em>b, H</em>c)}{\operatorname{LCM}(H<em>a, H</em>b) \cdot\operatorname{LCM}(H<em>a, H</em>c) \operatorname{LCM}(H<em>b, H</em>c)}$$</p><p>其中 $\operatorname{LCM}$ 表示的是最小公倍数函数。</p><p>小蓝想要使得三枚宝石组合后的精美程度 $S$ 尽可能的高，请你帮他找出精美程度最高的方案。如果存在多个方案 $S$ 值相同，优先选择按照 $H$ 值升序排列后字典序最小的方案。</p><p>输入格式：</p><p>第一行一个整数 $n$ 表示宝石个数。<br/>第二行有 $n$ 个整数 $H<em>1, H</em>2, \dots H_n$ 表示每个宝石的闪亮度。</p><p>输出格式：</p><p>输出一行包含三个整数表示满足条件的三枚宝石的 “闪亮度”。</p><p>数据范围与说明：</p><h3 id="">数据规模与约定</h3><ul><li>对 $30%$ 的数据，$n \leq 100$，$H_i \leq 10^3$。</li><li>对 $60%$ 的数据，$n \leq 2 \times 10^3$。</li><li>对全部的测试数据，保证 $3 \leq n \leq 10^5$，$1 \leq H_i \leq 10^5$。</li></ul><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5
1 2 3 4 9
</code></pre>
<p>输出：</p><pre class=""><code class="">1 2 3
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>在一个神秘的森林里，住着一个小精灵名叫小蓝。有一天，他偶然发现了一个隐藏在树洞里的宝藏，里面装满了闪烁着美丽光芒的宝石。这些宝石都有着不同的颜色和形状，但最引人注目的是它们各自独特的 “闪亮度” 属性。每颗宝石都有一个与生俱来的特殊能力，可以发出不同强度的闪光。小蓝共找到了 $n$ 枚宝石，第 $i$ 枚宝石的 “闪亮度” 属性值为 $H_i$，小蓝将会从这 $n$ 枚宝石中选出三枚进行组合，组合之后的精美程度 $S$ 可以用以下公式来衡量：</p><p>$$S = H<em>a H</em>b H<em>c \cdot \frac{\operatorname{LCM}(H</em>a, H<em>b, H</em>c)}{\operatorname{LCM}(H<em>a, H</em>b) \cdot\operatorname{LCM}(H<em>a, H</em>c) \operatorname{LCM}(H<em>b, H</em>c)}$$</p><p>其中 $\operatorname{LCM}$ 表示的是最小公倍数函数。</p><p>小蓝想要使得三枚宝石组合后的精美程度 $S$ 尽可能的高，请你帮他找出精美程度最高的方案。如果存在多个方案 $S$ 值相同，优先选择按照 $H$ 值升序排列后字典序最小的方案。</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
const int N=1e5+10;
int a[N],cnt[N];

int gcd(int a,int b){
    return b?gcd(b,a%b):a;
}

int lcm(int a,int b){
    return a*b/gcd(a,b);
}

int main(){
    int n;
    cin&gt;&gt;n;
    for(int i=1;i&lt;=n;i++){
        cin&gt;&gt;a[i];
        for(int j=1;j&lt;=sqrt(a[i]);j++){
            if(a[i]%j==0){
                cnt[j]++;
                if(j!=sqrt(a[i]))   cnt[a[i]/j]++;
            }
        }
    }
    int x=1;
    for(int i=N-10;i&gt;=1;i--){
        if(cnt[i]&gt;=3){
            x=i;
            break;
        }
    }
    sort(a+1,a+n+1);
    
    int c=0;
    for(int i=1;i&lt;=n;i++){
        if(a[i]%x==0){
            cout&lt;&lt;a[i]&lt;&lt;&quot; &quot;;
            c++;
            if(c==3)    return 0;
        }
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10426%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10426%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sun, 02 Mar 2025 08:50:38 GMT</pubDate></item><item><title><![CDATA[【题解】P10425]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10425%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10425%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p10425httpswwwluogucomcnproblemp10425">P10425（<a href="https://www.luogu.com.cn/problem/P10425">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>小蓝最近在研究一种浮点数的表示方法：$R$ 格式。对于一个大于 $0$ 的浮点数 $d$，可以用 $R$ 格式的整数来表示。给定一个转换参数 $n$，将浮点数转换为 $R$ 格式整数的做法是：</p><ol start="1"><li>将浮点数乘以 $2^n$。</li><li>四舍五入到最接近的整数。</li></ol><p>输入格式：</p><p>一行一个整数 $n$ 和一个浮点数 $d$。</p><p>输出格式：</p><p>一行一个整数表示 $d$ 用 $R$ 格式表示出的值。</p><p>数据范围与说明：</p><h3 id="-1-">样例 1 解释</h3><p>$3.14 \times 2^2 = 12.56$，四舍五入后为 $13$。</p><h3 id="">数据规模与约定</h3><p>用 $t$ 表示将 $d$ 视为字符串时的长度。</p><ul><li>对于 $50%$ 的数据，保证 $n \le 10$，$t \le 15$。</li><li>对于全部的测试数据，保证 $1 \le n \le 1000$，$1 \le t \le 1024$，保证 $d$ 是小数，即包含小数点。</li></ul><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">2 3.14
</code></pre>
<p>输出：</p><pre class=""><code class="">13
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>小蓝最近在研究一种浮点数的表示方法：$R$ 格式。对于一个大于 $0$ 的浮点数 $d$，可以用 $R$ 格式的整数来表示。给定一个转换参数 $n$，将浮点数转换为 $R$ 格式整数的做法是：</p><ol start="1"><li>将浮点数乘以 $2^n$。</li><li>四舍五入到最接近的整数。</li></ol><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int n,l;
string d;
int a[1000000];
void func(){
    for(int i=1;i&lt;=l;i++){
        a[i]*=2;
    }
    for(int i=1;i&lt;=l;i++){
        a[i+1]+=a[i]/10;
        a[i]%=10;
    }
    if(a[l+1]){
        l++;
    }
}
int main(){
    cin&gt;&gt;n&gt;&gt;d;
    reverse(d.begin(),d.end());
    int index=d.find(&quot;.&quot;);
    d.erase(index,1);
    l=d.size();
    for(int i=0;i&lt;l;i++) a[i+1]=d[i]-&#x27;0&#x27;;
    for(int i=1;i&lt;=n;i++){
        func();
    }
    if(a[index]&gt;=5){
a[index+1]++;
    }
    for(int i=index+1;i&lt;=l;i++)
{
a[i+1]+=a[i]/10;
a[i]%=10;
}
if(a[l+1])  l++;
for(int i=l;i&gt;index;i--)
cout&lt;&lt;a[i];
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10425%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10425%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 01 Mar 2025 12:15:02 GMT</pubDate></item><item><title><![CDATA[【题解】P10902]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10902%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10902%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p10902httpswwwluogucomcnproblemp10902">P10902（<a href="https://www.luogu.com.cn/problem/P10902">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>小蓝在无聊时随机生成了一个长度为 $n$ 的整数数组，数组中的第 $i$ 个数为 $a<em>i$，他觉得随机生成的数组不太美观，想把它变成回文数组，也是就对于任意 $i\in [1,n]$ 满足 $a</em>i=a_{n-i+1}$。小蓝一次操作可以指定相邻的两个数，将它们一起加 $1$ 或减 $1$；也可以只指定一个数加 $1$ 或减 $1$，请问他最少需要操作多少次能把这个数组变成回文数组？</p><p>输入格式：</p><p>输入的第一行包含一个正整数 $n$。</p><p>第二行包含 $n$ 个整数 $a<em>1, a</em>2,\cdots, a_n$，相邻整数之间使用一个空格分隔。</p><p>输出格式：</p><p>输出一行包含一个整数表示答案。</p><p>数据范围与说明：</p><p><strong>【样例说明】</strong></p><p>第一次操作将 $a<em>1, a</em>2$ 加 $1$，变为 $2, 3, 3, 4$；</p><p>后面两次操作将 $a_1$ 加 $1$，变为 $4,3,3,4$。</p><p><strong>【评测用例规模与约定】</strong></p><p>对于 $20%$ 的评测用例，$1 \le n \le 10$；<br/>对于所有评测用例，$1 \le n \le 10^5$，$-10^6 \le a_i \le 10^6$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">4
1 2 3 4
</code></pre>
<p>输出：</p><pre class=""><code class="">3
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>小蓝在无聊时随机生成了一个长度为 $n$ 的整数数组，数组中的第 $i$ 个数为 $a<em>i$，他觉得随机生成的数组不太美观，想把它变成回文数组，也是就对于任意 $i\in [1,n]$ 满足 $a</em>i=a_{n-i+1}$。小蓝一次操作可以指定相邻的两个数，将它们一起加 $1$ 或减 $1$；也可以只指定一个数加 $1$ 或减 $1$，请问他最少需要操作多少次能把这个数组变成回文数组？</p><h2 id="">代码</h2><h3 id="c">C</h3><pre class="language-c lang-c"><code class="language-c lang-c">#include&lt;bits/stdc++.h&gt;
using namespace std;
#define endl &#x27;\n&#x27;
const int N=1e5+10;
long long a[N],b[N],ans=0;
int main(){
    ios_sync_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin&gt;&gt;n;
    for(int i=0;i&lt;n;i++)    cin&gt;&gt;a[i];
    for(int i=0;i&lt;(n+1)/2;i++){
        if(a[i]&gt;a[n-i+1]){
            b[i]=a[n-i+1]-a[i];
        }else{
            b[n-i+1]=a[i]-a[n-i+1];
        }
    }
    for(int i=0;i&lt;n;i++){
        long long m=min(b[i],b[i+1]);
        b[i]-=m;
        b[i+1]-=m;
        ans+=m;
        ans+=b[i];
    }
    cout&lt;&lt;ans&lt;&lt;endl;
    return 0;
}
</code></pre>
<h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
#define endl &#x27;\n&#x27;
const int N=1e5+10;
long long a[N],b[N],ans=0;
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin&gt;&gt;n;
    for(int i=1;i&lt;=n;i++)    cin&gt;&gt;a[i];
    for(int i=1;i&lt;=(n+1)/2;i++){
        if(a[i]&lt;a[n-i+1]){
            b[i]=a[n-i+1]-a[i];
        }else{
            b[n-i+1]=a[i]-a[n-i+1];
        }
    }
    for(int i=1;i&lt;=n;i++){
        long long m=min(b[i],b[i+1]);
        b[i]-=m;
        b[i+1]-=m;
        ans+=m;
        ans+=b[i];
    }
    cout&lt;&lt;ans&lt;&lt;endl;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10902%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10902%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Thu, 27 Feb 2025 09:22:50 GMT</pubDate></item><item><title><![CDATA[【题解】P11041]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P11041%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P11041%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p11041httpswwwluogucomcnproblemp11041">P11041（<a href="https://www.luogu.com.cn/problem/P11041">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>小蓝和朋友们在玩一个报数游戏。由于今年是 $2024$ 年，他们决定要从小到大轮流报出是 $20$ 或 $24$ 倍数的正整数。前 $10$ 个被报出的数是：$20,24,40,48,60,72,80,96,100,120$。请问第 $202420242024$ 个被报出的数是多少？</p><p>这是一道结果填空的题，你只需要算出结果后提交即可。本题的结果为一个整数，在提交答案时只输出这个整数，填写多余的内容将无法得分。</p><p>输入格式：</p><p>本题无输入。</p><p>输出格式：</p><p>一行一个整数，表示你算出的答案。</p><p>数据范围与说明：</p><h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
typedef long long ll;
int main(){
    printf(&quot;2429042904288&quot;);
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P11041%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P11041%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 26 Feb 2025 10:11:09 GMT</pubDate></item><item><title><![CDATA[蓝桥杯 2023 省赛 - 岛屿个数]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/%E8%93%9D%E6%A1%A5%E6%9D%AF%E5%B2%9B%E5%B1%BF%E4%B8%AA%E6%95%B0">https://binzi.top/posts/%E7%AE%97%E6%B3%95/%E8%93%9D%E6%A1%A5%E6%9D%AF%E5%B2%9B%E5%B1%BF%E4%B8%AA%E6%95%B0</a></blockquote><div><h1 id="-2023-httpswwwlanqiaocnproblems3513learning"><a href="https://www.lanqiao.cn/problems/3513/learning/">蓝桥杯 2023 岛屿个数</a></h1><h2 id="">题目描述</h2><p>给定  组测试数据，每组数据是一张  的二值地图，字符 <code>1</code> 表示陆地，字符 <code>0</code> 表示海洋，地图之外视为海水。两个相邻的 <code>1</code>（上、下、左、右四个方向）属于同一个岛屿。若某个岛屿完全被另一个岛屿包围，则称其为子岛屿；统计时只需统计最外层岛屿的数量，子岛屿不计入答案。</p><h2 id="">输入格式</h2><p>第一行一个整数 。<br/>接下来每组数据：</p><ul><li>第一行两个整数 ；</li><li>接下来  行，每行一个长度为  的字符串。</li></ul><h2 id="">输出格式</h2><p>对每组数据输出一个整数，表示非子岛屿的数量。</p><h2 id="">思路</h2><ol start="1"><li>先从地图边界出发，使用 BFS/DFS 遍历海洋，标记所有与外海相连的海水；</li><li>在外海遍历过程中，如果遇到未访问的陆地，就说明发现了一个新的最外层岛屿，对其进行 BFS/DFS 并标记整座岛；</li><li>如果地图中没有与外海相连的海水，说明整张图都是陆地——此时答案为 1。</li></ol><h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;

const int N = 55;
int grid[N][N];
bool visSea[N][N], visLand[N][N];
int dx4[4] = {-1, 0, 1, 0};
int dy4[4] = {0, -1, 0, 1};
int dx8[8] = {-1,-1,-1,0,0,1,1,1};
int dy8[8] = {-1,0,1,-1,1,-1,0,1};
int n, m;

inline bool inside(int x, int y) {
    return x &gt;= 0 &amp;&amp; x &lt; n &amp;&amp; y &gt;= 0 &amp;&amp; y &lt; m;
}

void bfsLand(int sx, int sy) {
    queue&lt;pair&lt;int,int&gt;&gt; q;
    q.push({sx, sy});
    visLand[sx][sy] = true;
    while (!q.empty()) {
        auto [x, y] = q.front();
        q.pop();
        for (int d = 0; d &lt; 4; ++d) {
            int nx = x + dx4[d], ny = y + dy4[d];
            if (inside(nx, ny) &amp;&amp; grid[nx][ny] &amp;&amp; !visLand[nx][ny]) {
                visLand[nx][ny] = true;
                q.push({nx, ny});
            }
        }
    }
}

int bfsSea(int sx, int sy) {
    queue&lt;pair&lt;int,int&gt;&gt; q;
    q.push({sx, sy});
    visSea[sx][sy] = true;
    int islands = 0;
    while (!q.empty()) {
        auto [x, y] = q.front();
        q.pop();
        for (int d = 0; d &lt; 8; ++d) {
            int nx = x + dx8[d], ny = y + dy8[d];
            if (!inside(nx, ny)) continue;
            if (!grid[nx][ny] &amp;&amp; !visSea[nx][ny]) {
                visSea[nx][ny] = true;
                q.push({nx, ny});
            } else if (grid[nx][ny] &amp;&amp; !visLand[nx][ny]) {
                ++islands;
                bfsLand(nx, ny);
            }
        }
    }
    return islands;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int T; cin &gt;&gt; T;
    while (T--) {
        cin &gt;&gt; n &gt;&gt; m;
        for (int i = 0; i &lt; n; ++i) {
            string s; cin &gt;&gt; s;
            for (int j = 0; j &lt; m; ++j) grid[i][j] = s[j] - &#x27;0&#x27;;
        }
        memset(visSea, 0, sizeof(visSea));
        memset(visLand, 0, sizeof(visLand));

        int ans = 0;
        bool touched = false;
        for (int i = 0; i &lt; n; ++i) {
            for (int j = 0; j &lt; m; ++j) {
                if (i == 0 || i == n - 1 || j == 0 || j == m - 1) {
                    if (!grid[i][j] &amp;&amp; !visSea[i][j]) {
                        touched = true;
                        ans += bfsSea(i, j);
                    }
                }
            }
        }
        if (!touched) ans = 1;
        cout &lt;&lt; ans &lt;&lt; &#x27;\n&#x27;;
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/%E8%93%9D%E6%A1%A5%E6%9D%AF%E5%B2%9B%E5%B1%BF%E4%B8%AA%E6%95%B0</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/%E8%93%9D%E6%A1%A5%E6%9D%AF%E5%B2%9B%E5%B1%BF%E4%B8%AA%E6%95%B0</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 25 Feb 2025 01:00:00 GMT</pubDate></item><item><title><![CDATA[【题解】P1706]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1706%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1706%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1706">P1706</h1><h2 id="">题目</h2><p>题目描述：</p><p>按照字典序输出自然数 $1$ 到 $n$ 所有不重复的排列，即 $n$ 的全排列，要求所产生的任一数字序列中不允许出现重复的数字。</p><p>输入格式：</p><p>一个整数 $n$。</p><p>输出格式：</p><p>由 $1 \sim n$ 组成的所有不重复的数字序列，每行一个序列。</p><p>每个数字保留 $5$ 个场宽。</p><p>数据范围与说明：</p><p>$1 \leq n \leq 9$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">3
</code></pre>
<p>输出：</p><pre class=""><code class="">1    2    3
1    3    2
2    1    3
2    3    1
3    1    2
3    2    1
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
bool a[100];
int n;
void dfs(int pos,vector&lt;int&gt;&amp;arr){
    if(pos==n){
        for(int &amp;x:arr) cout&lt;&lt;setw(5)&lt;&lt;x;
        cout&lt;&lt;endl;
        return ;
    }
    for(int i=1;i&lt;=n;i++){
        if(!a[i]){
            a[i]=true;
            arr.push_back(i);
            dfs(pos+1,arr);
            arr.pop_back();
            a[i]=false;
        }
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    vector&lt;int&gt;arr;
    cin&gt;&gt;n;
    dfs(0,arr);
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1706%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1706%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 24 Feb 2025 15:20:27 GMT</pubDate></item><item><title><![CDATA[【题解】P1157]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1157%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1157%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1157">P1157</h1><h2 id="">题目</h2><p>题目描述：</p><p>排列与组合是常用的数学方法，其中组合就是从 $n$ 个元素中抽出 $r$ 个元素（不分顺序且 $r \le n$），我们可以简单地将 $n$ 个元素理解为自然数 $1,2,\dots,n$，从中任取 $r$ 个数。</p><p>现要求你输出所有组合。</p><p>例如 $n=5,r=3$，所有组合为：</p><p>$123,124,125,134,135,145,234,235,245,345$。</p><p>输入格式：</p><p>一行两个自然数 $n,r(1&lt;n&lt;21,0 \le r \le n)$。</p><p>输出格式：</p><p>所有的组合，每一个组合占一行且其中的元素按由小到大的顺序排列，每个元素占三个字符的位置，所有的组合也按字典顺序。</p><p><strong>注意哦！输出时，每个数字需要 $3$ 个场宽。以 C++ 为例，你可以使用下列代码：</strong></p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">cout &lt;&lt; setw(3) &lt;&lt; x;
</code></pre>
<p>输出占 $3$ 个场宽的数 $x$。注意你需要头文件 <code>iomanip</code>。</p><p>数据范围与说明：</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5 3
</code></pre>
<p>输出：</p><pre class=""><code class="">1  2  3
1  2  4
1  2  5
1  3  4
1  3  5
1  4  5
2  3  4
2  3  5
2  4  5
3  4  5
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int n,r;
void dfs(int pos,int start,vector&lt;int&gt; &amp;tmp){
    if(pos==r){
        for(int &amp;x:tmp){
            cout&lt;&lt;setw(3)&lt;&lt;x;
        }
        cout&lt;&lt;endl;
        return;
    }
    for(int i=start;i&lt;=n;i++){
        tmp.push_back(i);
        dfs(pos+1,i+1,tmp);
        tmp.pop_back();
    }
}
int main(){
    vector&lt;int&gt;a;
    cin&gt;&gt;n&gt;&gt;r;
    dfs(0,1,a);
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1157%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1157%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 24 Feb 2025 14:14:43 GMT</pubDate></item><item><title><![CDATA[【题解】P1036 [NOIP 2002 普及组] 选数]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1036%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1036%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1036-noip-2002--httpswwwluogucomcnproblemp1036">P1036 [NOIP 2002 普及组] 选数（<a href="https://www.luogu.com.cn/problem/P1036">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>已知 $n$ 个整数 $x<em>1,x</em>2,\cdots,x_n$，以及 $1$ 个整数 $k$（$k&lt;n$）。从 $n$ 个整数中任选 $k$ 个整数相加，可分别得到一系列的和。例如当 $n=4$，$k=3$，$4$ 个整数分别为 $3,7,12,19$ 时，可得全部的组合与它们的和为：</p><p>$3+7+12=22$</p><p>$3+7+19=29$</p><p>$7+12+19=38$</p><p>$3+12+19=34$</p><p>现在，要求你计算出和为素数共有多少种。</p><p>例如上例，只有一种的和为素数：$3+7+19=29$。</p><p>输入格式：</p><p>第一行两个空格隔开的整数 $n,k$（$1 \le n \le 20$，$k&lt;n$）。</p><p>第二行 $n$ 个整数，分别为 $x<em>1,x</em>2,\cdots,x<em>n$（$1 \le x</em>i \le 5\times 10^6$）。</p><p>输出格式：</p><p>输出一个整数，表示种类数。</p><p>数据范围与说明：</p><p><strong>【题目来源】</strong></p><p>NOIP 2002 普及组第二题</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">4 3
3 7 12 19
</code></pre>
<p>输出：</p><pre class=""><code class="">1
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>已知 $n$ 个整数 $x<em>1,x</em>2,\cdots,x_n$，以及 $1$ 个整数 $k$（$k&lt;n$）。从 $n$ 个整数中任选 $k$ 个整数相加，可分别得到一系列的和。例如当 $n=4$，$k=3$，$4$ 个整数分别为 $3,7,12,19$ 时，可得全部的组合与它们的和为：</p><p>$3+7+12=22$</p><p>$3+7+19=29$</p><p>$7+12+19=38$</p><p>$3+12+19=34$</p><p>现在，要求你计算出和为素数共有多少种。</p><p>例如上例，只有一种的和为素数：$3+7+19=29$。</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
typedef long long ll;
ll n,a[25],k,ans=0,sum[25];
bool check(ll x){
    if(x==1)    return false;
    for(ll i=2;i&lt;x;i++){
        if(x%i==0)  return false;
    }
    return true;
}

void solve(ll total,ll pos,ll start){
    if(pos==k){
        if(check(total)){
            ans++;
        }
        return ;
    }
    
    for(int i=start;i&lt;n;i++){
        solve(total+a[i],pos+1,i+1);
    }
    return ;
}

int main(){
    cin&gt;&gt;n&gt;&gt;k;
    for(int i=0;i&lt;n;i++){
        cin&gt;&gt;a[i];
    }
    solve(0,0,0);
    cout&lt;&lt;ans;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1036%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1036%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sun, 23 Feb 2025 14:14:48 GMT</pubDate></item><item><title><![CDATA[【题解】P1618 三连击（升级版）]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1618%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1618%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1618-httpswwwluogucomcnproblemp1618">P1618 三连击（升级版）（<a href="https://www.luogu.com.cn/problem/P1618">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>将 $1, 2,\ldots, 9$ 共 $9$ 个数分成三组，分别组成三个三位数，且使这三个三位数的比例是 $A:B:C$，试求出所有满足条件的三个三位数，若无解，输出 <code>No!!!</code>。</p><p>//感谢黄小U饮品完善题意</p><p>输入格式：</p><p>三个数，$A,B,C$。</p><p>输出格式：</p><p>若干行，每行 $3$ 个数字。按照每行第一个数字升序排列。</p><p>数据范围与说明：</p><p>保证 $0 \le A&lt;B&lt;C \le 999$。</p><hr/><p>$\text{upd 2022.8.3}$：新增加二组 Hack 数据。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">1 2 3
</code></pre>
<p>输出：</p><pre class=""><code class="">192 384 576
219 438 657
273 546 819
327 654 981
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int a,b,c,x[10];
bool used[10]={0},ans=false;
int func(int s){
    int sum=0;
    for(int i=3*s-2;i&lt;=3*s;i++){
        sum*=10;
        sum+=x[i];
    }
    return sum;
}
void solve(int n){
    if(n==10&amp;&amp;(func(1)*b==func(2)*a)&amp;&amp;(func(3)*a==func(1)*c)){
        cout&lt;&lt;func(1)&lt;&lt;&quot; &quot;&lt;&lt;func(2)&lt;&lt;&quot; &quot;&lt;&lt;func(3)&lt;&lt;endl;
        ans=true;
        return ;
    }
    for(int i=1;i&lt;=9;i++){
        if(!used[i]){
            x[n]=i;
            used[i]=true;
            solve(n+1);
            used[i]=false;
        }
    }
    return ;
}
int main(){
    cin&gt;&gt;a&gt;&gt;b&gt;&gt;c;
    solve(1);
    if(!ans)    cout&lt;&lt;&quot;No!!!&quot;;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1618%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1618%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sun, 23 Feb 2025 12:39:30 GMT</pubDate></item><item><title><![CDATA[烤鸡 - 题解]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2089%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2089%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="httpswwwluogucomcnproblemp2089">烤鸡（<a href="https://www.luogu.com.cn/problem/P2089">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>猪猪 Hanke 特别喜欢吃烤鸡（本是同畜牲，相煎何太急！）Hanke 吃鸡很特别，为什么特别呢？因为他有 $10$ 种配料（芥末、孜然等），每种配料可以放 $1$ 到 $3$ 克，任意烤鸡的美味程度为所有配料质量之和。</p><p>现在， Hanke 想要知道，如果给你一个美味程度 $n$ ，请输出这 $10$ 种配料的所有搭配方案。</p><p>输入格式：</p><p>一个正整数 $n$，表示美味程度。</p><p>输出格式：</p><p>第一行，方案总数。</p><p>第二行至结束，$10$ 个数，表示每种配料所放的质量，按字典序排列。</p><p>如果没有符合要求的方法，就只要在第一行输出一个 $0$。</p><p>数据范围与说明：</p><p>对于 $100%$ 的数据，$n \leq 10000$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">11
</code></pre>
<p>输出：</p><pre class=""><code class="">10
1 1 1 1 1 1 1 1 1 2 
1 1 1 1 1 1 1 1 2 1 
1 1 1 1 1 1 1 2 1 1 
1 1 1 1 1 1 2 1 1 1 
1 1 1 1 1 2 1 1 1 1 
1 1 1 1 2 1 1 1 1 1 
1 1 1 2 1 1 1 1 1 1 
1 1 2 1 1 1 1 1 1 1 
1 2 1 1 1 1 1 1 1 1 
2 1 1 1 1 1 1 1 1 1
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int n,kind,m1[10000][10],m2[10];
void func(int total,int a){
    if(a==10){
        if(total==n){
            for (int j = 0; j &lt; 10; j++) {
                m1[kind][j] = m2[j];
            }   
            kind++;
        }
        return ;
    }
    if(total&gt;=n)    return;
    for(int i=1;i&lt;=3;i++){
        m2[a]=i;
        func(total+i,a+1);
    }     
}
int main(){
    cin&gt;&gt;n;
    if (n &lt; 10 || n &gt; 30) {
        cout &lt;&lt;&quot;0&quot;&lt;&lt; endl;
        return 0;
    }
    func(0,0);
    cout&lt;&lt;kind&lt;&lt;endl;
    for(int i=0;i&lt;kind;i++){
        for(int j=0;j&lt;10;j++){
            cout&lt;&lt;m1[i][j]&lt;&lt;&quot; &quot;;
        }
        cout&lt;&lt;endl;
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2089%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2089%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sun, 23 Feb 2025 08:02:10 GMT</pubDate></item><item><title><![CDATA[【题解】P2241]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2241%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2241%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p2241">P2241</h1><h2 id="">题目</h2><p>题目描述：</p><p>有一个 $n \times m$ 方格的棋盘，求其方格包含多少正方形、长方形（不包含正方形）。</p><p>输入格式：</p><p>一行，两个正整数 $n,m$（$n \leq 5000,m \leq 5000$）。</p><p>输出格式：</p><p>一行，两个正整数，分别表示方格包含多少正方形、长方形（不包含正方形）。</p><p>数据范围与说明：</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">2 3
</code></pre>
<p>输出：</p><pre class=""><code class="">8 10
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
long long n,m,sqr,rec;
int main(){
    cin&gt;&gt;n&gt;&gt;m;
    for(int i=0;i&lt;n;i++){
        for(int j=0;j&lt;m;j++){
            if(i==j)    sqr+=(n-i)*(m-j);
            else    rec+=(n-i)*(m-j);
        }
    }
    cout&lt;&lt;sqr&lt;&lt;&quot; &quot;&lt;&lt;rec&lt;&lt;endl;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2241%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2241%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 21 Feb 2025 10:34:40 GMT</pubDate></item><item><title><![CDATA[【题解】P1601 高精度加法]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1601%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1601%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1601-httpswwwluogucomcnproblemp1601">P1601 高精度加法（<a href="https://www.luogu.com.cn/problem/P1601">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>给定两个非负整数 $a,b$，求它们的和。<strong>不用考虑负数</strong>。</p><p>输入格式：</p><p>输入共两行，每行一个非负整数，分别为 $a,b$。</p><p>输出格式：</p><p>输出一行一个非负整数，表示 $a+b$ 的值。</p><p>数据范围与说明：</p><p>对于 $20%$ 的测试数据，$a,b \le 10^9$；<br/>对于 $40%$ 的测试数据，$a,b \le 10^{18}$；<br/>对于 $100%$ 的测试数据，$0\le a,b \le 10^{500}$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">1
1
</code></pre>
<p>输出：</p><pre class=""><code class="">2
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">1001
9099
</code></pre>
<p>输出：</p><pre class=""><code class="">10100
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
string a,b,result=&quot;&quot;;
string add(string a,string b){
    if(a.size()&lt;b.size()){
        swap(a,b);
    }
    int len_a=a.size();
    int len_b=b.size();
    int diff=len_a-len_b;
    int carry=0;
    for(int i=a.size()-1;i&gt;=0;i--){
        int digit1=a[i]-&#x27;0&#x27;;    
        int digit2=(i&gt;=diff)?(b[i-diff]-&#x27;0&#x27;):0;
        int sum=digit1+digit2+carry;
        carry=sum/10;
        result.push_back(sum%10+&#x27;0&#x27;);
    }
    if(carry){
        result.push_back(carry+&#x27;0&#x27;);
    }
    reverse(result.begin(),result.end());
    return result;
}
int main(){
    cin&gt;&gt;a;
    cin&gt;&gt;b;
    string num=add(a,b);
    cout&lt;&lt;num;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1601%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1601%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 18 Feb 2025 11:28:12 GMT</pubDate></item><item><title><![CDATA[【题解】P1307]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1307%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1307%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1307">P1307</h1><h2 id="">题目</h2><p>题目描述：</p><p>给定一个整数 $N$，请将该数各个位上数字反转得到一个新数。新数也应满足整数的常见形式，即除非给定的原数为零，否则反转后得到的新数的最高位数字不应为零（参见样例 2）。</p><p>输入格式：</p><p>一个整数 $N$。</p><p>输出格式：</p><p>一个整数，表示反转后的新数。</p><p>数据范围与说明：</p><p><strong>【数据范围】</strong></p><p>。</p><p>noip2011 普及组第一题</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">123
</code></pre>
<p>输出：</p><pre class=""><code class="">321
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">-380
</code></pre>
<p>输出：</p><pre class=""><code class="">-83
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int main(){
    string s;
    cin&gt;&gt;s;
    if(s[0]==&#x27;0&#x27;)   cout&lt;&lt;0;
    while(s.back()==&#x27;0&#x27;&amp;&amp;!s.empty()){
        s.erase(s.size()-1);
    }
    reverse(s.begin(),s.end());
    if(s.back()==&#x27;-&#x27;){
        s.erase(s.size()-1);
        cout&lt;&lt;&quot;-&quot;&lt;&lt;s;
    }else{
        cout&lt;&lt;s;
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1307%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1307%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 10 Feb 2025 14:27:28 GMT</pubDate></item><item><title><![CDATA[【深基2.习6】Apples Prologue / 苹果和虫子 - 题解]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5709%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5709%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="26apples-prologue--httpswwwluogucomcnproblemp5709">【深基2.习6】Apples Prologue / 苹果和虫子（<a href="https://www.luogu.com.cn/problem/P5709">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>小 B 喜欢吃苹果。她现在有 $m$（$1 \le m \le 100$）个苹果，吃完一个苹果需要花费 $t$（$0 \le t \le 100$）分钟，吃完一个后立刻开始吃下一个。现在时间过去了 $s$（$1 \le s \le 10000$）分钟，请问她还有几个完整的苹果？</p><p>输入格式：</p><p>输入三个<strong>非负整数</strong>表示 $m, t, s$。</p><p>输出格式：</p><p>输出一个整数表示答案。</p><p>数据范围与说明：</p><p>如果你出现了 RE，不如检查一下被零除？</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">50 10 200
</code></pre>
<p>输出：</p><pre class=""><code class="">30
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int main(){
    int m,t,s;
    cin&gt;&gt;m&gt;&gt;t&gt;&gt;s;
    if(t==0)
    {
        cout&lt;&lt;0;
        return 0;
    }
    if(s%t==0){
        cout&lt;&lt;max(m-s/t,0);
    }else{
        cout&lt;&lt;max(m-s/t-1,0);
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5709%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5709%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 10 Feb 2025 13:22:40 GMT</pubDate></item><item><title><![CDATA[【题解】P1567]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1567%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1567%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1567">P1567</h1><h2 id="">题目</h2><p>题目描述：</p><p>炎热的夏日，KC 非常的不爽。他宁可忍受北极的寒冷，也不愿忍受厦门的夏天。最近，他开始研究天气的变化。他希望用研究的结果预测未来的天气。</p><p>经历千辛万苦，他收集了连续 $N(1 \leq N \leq 10^6)$ 天的最高气温数据。</p><p>现在，他想知道最高气温一直上升的最长连续天数。</p><p>输入格式：</p><p>第 1 行：一个整数 $N$ 。$1 \leq N \leq 10^6$</p><p>第 2 行：$N$个空格隔开的整数，表示连续 $N$ 天的最高气温。$0 \leq$ 最高气温 $\leq 10^9$ 。</p><p>输出格式：</p><p>1 行：一个整数，表示最高气温一直上升的最长连续天数。</p><p>数据范围与说明：</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">10
1 2 3 2 4 5 6 8 5 9
</code></pre>
<p>输出：</p><pre class=""><code class="">5
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int a[1000005];
int main(){
    int n,ans=1;
    cin&gt;&gt;n;
    for(int i=1;i&lt;=n;i++){
        cin&gt;&gt;a[i];
    }
    int count=1;
    for(int i=2;i&lt;=n;i++){
        if(a[i]&gt;a[i-1]){
            count++;
            ans=max(ans,count);
        }else{
            count=1;
        }
    }
    cout&lt;&lt;ans;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1567%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1567%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sun, 09 Feb 2025 19:06:26 GMT</pubDate></item><item><title><![CDATA[【题解】P1506]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1506%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1506%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1506">P1506</h1><h2 id="">题目</h2><p>题目描述：</p><p>oibh 被突来的洪水淹没了，还好 oibh 总部有在某些重要的地方起一些围墙。用 <code>*</code> 号表示，而一个四面被围墙围住的区域洪水是进不去的。</p><p>oibh 总部内部也有许多重要区域，每个重要区域在图中用一个 <code>0</code> 表示。</p><p>现在给出 oibh 的围墙建设图，问有多少个没被洪水淹到的重要区域。</p><p>输入格式：</p><p>第一行为两个正整数 $x,y$。</p><p>接下来 $x$ 行，每行 $y$ 个字符，由 <code>*</code> 和 <code>0</code> 组成，表示 oibh 总部的建设图。</p><p>输出格式：</p><p>输出没被水淹没的 oibh 总部的 <code>0</code> 的数量。</p><p>数据范围与说明：</p><p>对于 $100%$ 的数据，$1 \le x,y \le 500$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">4 5
00000
00*00
0*0*0
00*00
</code></pre>
<p>输出：</p><pre class=""><code class="">1
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">5 5
*****
*0*0*
**0**
*0*0*
*****
</code></pre>
<p>输出：</p><pre class=""><code class="">5
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
int n,m;
int a[501][501];
int kx[5]={0,1,-1,0,0}; 
int ky[5]={0,0,0,1,-1};
void dfs(int x,int y){
    a[x][y]=1;
    for(int i=1;i&lt;=4;i++){
        int x0=x+kx[i];
        int y0=y+ky[i];
        if(x0&gt;0&amp;&amp;x0&lt;=n&amp;&amp;y0&gt;0&amp;&amp;y0&lt;=m&amp;&amp;a[x0][y0]==0)  dfs(x0,y0);
    }
}
int main(){
    char e;
    cin&gt;&gt;n&gt;&gt;m;
    for(int i=1;i&lt;=n;i++){
        for(int j=1;j&lt;=m;j++){
            cin&gt;&gt;e;
            if(e==&#x27;*&#x27;)  a[i][j]=1;
            else    a[i][j]=0;
        }
    }
    for(int i=1;i&lt;=n;i++){
        if(a[i][1]==0) dfs(i,1);
        if(a[i][m]==0) dfs(i,m); 
    }
    for(int i=1;i&lt;=m;i++){
        if(a[1][i]==0) dfs(1,i);
        if(a[n][i]==0) dfs(n,i); 
    }
    int s=0;
    for(int i=1;i&lt;=n;i++){
        for(int j=1;j&lt;=m;j++){
            if(a[i][j]==0)s++;
        }
    }
    cout&lt;&lt;s;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1506%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1506%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 08 Feb 2025 19:34:15 GMT</pubDate></item><item><title><![CDATA[【题解】P10442]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10442%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10442%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p10442httpswwwluogucomcnproblemp10442">P10442（<a href="https://www.luogu.com.cn/problem/P10442">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>给定字符串 $s,t$。</p><p>现在你要在 $s,t$ 中删除一些字符并将它们重新排列使 $s=t$。</p><p>问操作后的 $|s|$（即字符串 $s$ 的长度）最大是多少？</p><p>输入格式：</p><p>第一行一个字符串 $s$。</p><p>第二行一个字符串 $t$。</p><p>输出格式：</p><p>一行一个整数，表示操作后的 $|s|$ 的最大值。</p><p>数据范围与说明：</p><p>在第一个样例中，将 <code>a</code> 删除，留下 <code>bc</code>。</p><p>此时 $|s|=2$，可以证明这是最优解。</p><p>在第二个样例中，将 <code>aaaaa</code> 删除，留下空串。<br/>将 <code>bbbbb</code> 删除，留下空串。</p><p>此时 $|s|=0$，可以证明这是最优解。</p><p><strong>本题采用捆绑测试</strong>。</p><p>记 $n=\max(|s|,|t|)$。</p><table><thead><tr><th> $\text{Subtask}$ </th><th>  </th><th> 特殊性质 </th><th> 总分值 </th></tr></thead><tbody><tr><td> $1$ </td><td> $10$ </td><td> 无 </td><td> $25$ </td></tr><tr><td> $2$ </td><td> $10^5$ </td><td> $\text{A}$ </td><td> $25$ </td></tr><tr><td> $3$ </td><td> $10^5$ </td><td> $\text{B}$ </td><td> $25$ </td></tr><tr><td> $4$ </td><td> $10^5$ </td><td> 无 </td><td> $25$ </td></tr></tbody></table><p>对于 $100%$ 的数据，$1  \le |s|,|t| \le 10^5$，字符串均由小写字母组成。</p><p>特殊性质 $\text{A}$：$s$ 是一个 $\text{a}\sim\text{z}$ 的排列。</p><p>特殊性质 $\text{B}$：保证 $s<em>i,t</em>i\in{\text{a},\text{b} }$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">abc
bc
</code></pre>
<p>输出：</p><pre class=""><code class="">2
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">aaaaa
bbbbb
</code></pre>
<p>输出：</p><pre class=""><code class="">0
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>给定字符串 $s,t$。</p><p>现在你要在 $s,t$ 中删除一些字符并将它们重新排列使 $s=t$。</p><p>问操作后的 $|s|$（即字符串 $s$ 的长度）最大是多少？</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;

int main() {
    string s, t;
    cin &gt;&gt; s &gt;&gt; t;
    vector&lt;int&gt; count_s(26, 0), count_t(26, 0);

    for (char c : s) {
        count_s[c - &#x27;a&#x27;]++;
    }

    for (char c : t) {
        count_t[c - &#x27;a&#x27;]++;
    }

    int ans = 0;
    for (int i = 0; i &lt; 26; i++) {
        ans += min(count_s[i], count_t[i]);
    }
    cout &lt;&lt; ans &lt;&lt; endl;

    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10442%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10442%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 04 Feb 2025 10:22:33 GMT</pubDate></item><item><title><![CDATA[【题解】P1051]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1051%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1051%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1051httpswwwluogucomcnproblemp1051">P1051（<a href="https://www.luogu.com.cn/problem/P1051">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>某校的惯例是在每学期的期末考试之后发放奖学金。发放的奖学金共有五种，获取的条件各自不同：</p><ol start="1"><li>院士奖学金，每人 $8000$ 元，期末平均成绩高于 $80$ 分（$&gt;80$），并且在本学期内发表 $1$ 篇或 $1$ 篇以上论文的学生均可获得；</li><li>五四奖学金，每人 $4000$ 元，期末平均成绩高于 $85$ 分（$&gt;85$），并且班级评议成绩高于 $80$ 分（$&gt;80$）的学生均可获得；</li><li>成绩优秀奖，每人 $2000$ 元，期末平均成绩高于 $90$ 分（$&gt;90$）的学生均可获得；</li><li>西部奖学金，每人 $1000$ 元，期末平均成绩高于 $85$ 分（$&gt;85$）的西部省份学生均可获得；</li><li>班级贡献奖，每人 $850$ 元，班级评议成绩高于 $80$ 分（$&gt;80$）的学生干部均可获得；</li></ol><p>只要符合条件就可以得奖，每项奖学金的获奖人数没有限制，每名学生也可以同时获得多项奖学金。例如姚林的期末平均成绩是 $87$ 分，班级评议成绩 $82$ 分，同时他还是一位学生干部，那么他可以同时获得五四奖学金和班级贡献奖，奖金总数是 $4850$ 元。</p><p>现在给出若干学生的相关数据，请计算哪些同学获得的奖金总数最高（假设总有同学能满足获得奖学金的条件）。</p><p>输入格式：</p><p>第一行是 $1$ 个整数 $N$，表示学生的总数。</p><p>接下来的 $N$ 行每行是一位学生的数据，从左向右依次是姓名，期末平均成绩，班级评议成绩，是否是学生干部，是否是西部省份学生，以及发表的论文数。姓名是由大小写英文字母组成的长度不超过 $20$ 的字符串（不含空格）；期末平均成绩和班级评议成绩都是 $0$ 到 $100$ 之间的整数（包括 $0$ 和 $100$）；是否是学生干部和是否是西部省份学生分别用 $1$ 个字符表示，$\tt Y$ 表示是，$\tt N$ 表示不是；发表的论文数是 $0$ 到 $10$ 的整数（包括 $0$ 和 $10$）。每两个相邻数据项之间用一个空格分隔。</p><p>输出格式：</p><p>共 $3$ 行。</p><ul><li>第 $1$ 行是获得最多奖金的学生的姓名。如果有两位或两位以上的学生获得的奖金最多，输出他们之中在输入文件中出现最早的学生的姓名。</li><li>第 $2$ 行是这名学生获得的奖金总数。</li><li>第 $3$ 行是这 $N$ 个学生获得的奖学金的总数。</li></ul><p>数据范围与说明：</p><p><strong>【数据范围】</strong></p><p>对于 $100%$ 的数据，满足 $1 \le  N \le  100$。</p><p><strong>【题目来源】</strong></p><p>NOIP 2005 提高组第一题</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">4
YaoLin 87 82 Y N 0
ChenRuiyi 88 78 N Y 1
LiXin 92 88 N N 0
ZhangQin 83 87 Y N 1
</code></pre>
<p>输出：</p><pre class=""><code class="">ChenRuiyi
9000
28700
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>某校的惯例是在每学期的期末考试之后发放奖学金。发放的奖学金共有五种，获取的条件各自不同：</p><ol start="1"><li>院士奖学金，每人 $8000$ 元，期末平均成绩高于 $80$ 分（$&gt;80$），并且在本学期内发表 $1$ 篇或 $1$ 篇以上论文的学生均可获得；</li><li>五四奖学金，每人 $4000$ 元，期末平均成绩高于 $85$ 分（$&gt;85$），并且班级评议成绩高于 $80$ 分（$&gt;80$）的学生均可获得；</li><li>成绩优秀奖，每人 $2000$ 元，期末平均成绩高于 $90$ 分（$&gt;90$）的学生均可获得；</li><li>西部奖学金，每人 $1000$ 元，期末平均成绩高于 $85$ 分（$&gt;85$）的西部省份学生均可获得；</li><li>班级贡献奖，每人 $850$ 元，班级评议成绩高于 $80$ 分（$&gt;80$）的学生干部均可获得；</li></ol><p>只要符合条件就可以得奖，每项奖学金的获奖人数没有限制，每名学生也可以同时获得多项奖学金。例如姚林的期末平均成绩是 $87$ 分，班级评议成绩 $82$ 分，同时他还是一位学生干部，那么他可以同时获得五四奖学金和班级贡献奖，奖金总数是 $4850$ 元。</p><p>现在给出若干学生的相关数据，请计算哪些同学获得的奖金总数最高（假设总有同学能满足获得奖学金的条件）。</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int main(){
    int n,score1,score2,sum=0,max=0,total=0,paper;
    char a,b;
    string name,maxn;
    cin&gt;&gt;n;
    while(n--){
        cin&gt;&gt;name&gt;&gt;score1&gt;&gt;score2&gt;&gt;a&gt;&gt;b&gt;&gt;paper;
        if(score1&gt;80 &amp;&amp; paper&gt;0) 
            sum+=8000;
        if(score1&gt;85 &amp;&amp; score2&gt;80) 
            sum+=4000;
        if(score1&gt;90) 
            sum+=2000;
        if(score1&gt;85 &amp;&amp; b==&#x27;Y&#x27;)
            sum+=1000;
        if(score2&gt;80 &amp;&amp; a==&#x27;Y&#x27;)
            sum+=850;
        total+=sum;
        if(sum&gt;max)
            maxn=name,max=sum;
        sum=0;
    }
    cout&lt;&lt;maxn&lt;&lt;endl&lt;&lt;max&lt;&lt;endl&lt;&lt;total;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1051%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1051%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 24 Jan 2025 12:23:23 GMT</pubDate></item><item><title><![CDATA[【题解】P1035 [NOIP 2002 普及组] 级数求和]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1035%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1035%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1035-noip-2002--httpswwwluogucomcnproblemp1035">P1035 [NOIP 2002 普及组] 级数求和（<a href="https://www.luogu.com.cn/problem/P1035">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>已知：$S<em>n= 1+\dfrac{1}{2}+\dfrac{1}{3}+…+\dfrac{1}{n}$。显然对于任意一个整数 $k$，当 $n$ 足够大的时候，$S</em>n&gt;k$。</p><p>现给出一个整数 $k$，要求计算出一个最小的 $n$，使得 $S_n&gt;k$。</p><p>输入格式：</p><p>一个正整数 $k$。</p><p>输出格式：</p><p>一个正整数 $n$。</p><p>数据范围与说明：</p><p><strong>【数据范围】</strong></p><p>对于 $100%$ 的数据，$1\le k \le 15$。</p><p><strong>【题目来源】</strong></p><p>NOIP 2002 普及组第一题</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">1
</code></pre>
<p>输出：</p><pre class=""><code class="">2
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>已知：$S<em>n= 1+\dfrac{1}{2}+\dfrac{1}{3}+…+\dfrac{1}{n}$。显然对于任意一个整数 $k$，当 $n$ 足够大的时候，$S</em>n&gt;k$。</p><p>现给出一个整数 $k$，要求计算出一个最小的 $n$，使得 $S_n&gt;k$。</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int main(){
    int k;
    double sum=0;
    cin&gt;&gt;k;
    int n=1;
    while(true){
        sum+=(double)1/n;
        if(sum&gt;k){
            break;
        }
        n++;
    }
    cout&lt;&lt;n;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1035%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1035%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 24 Jan 2025 12:09:15 GMT</pubDate></item><item><title><![CDATA[Synchronized 与 ReentrantLock 的区别]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/java/Synchronized%20%E4%B8%8E%20ReentrantLock%20%E7%9A%84%E5%8C%BA%E5%88%AB">https://binzi.top/posts/java/Synchronized%20%E4%B8%8E%20ReentrantLock%20%E7%9A%84%E5%8C%BA%E5%88%AB</a></blockquote><div><h1 id="synchronized--reentrantlock-">Synchronized 与 ReentrantLock 的区别</h1><p><code>synchronized</code>：<strong>隐式锁</strong>，JVM 自动加锁 / 释放锁</p><p><code>ReentrantLock</code>：<strong>显式锁</strong>，需要手动 <code>lock()</code> / <code>unlock()</code></p><p>ReentrantLock 本质上是基于 AQS 实现的。</p><p>synchronized是非公平锁 然后ReentrantLock 有公平锁有非公平锁</p><p>ReentrantLock 支持中断<code>lock.lockInterruptibly();</code>还有超时获取锁</p><pre class="language-java lang-java"><code class="language-java lang-java">if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try { } finally { lock.unlock(); }
}
</code></pre>
<table><thead><tr><th>  </th><th> synchronized </th><th> ReentrantLock </th></tr></thead><tbody><tr><td> 锁类型 </td><td> 隐式锁 </td><td> 显式锁 </td></tr><tr><td> 释放方式 </td><td> 自动 </td><td> 手动 </td></tr><tr><td> 公平锁 </td><td> 不支持 </td><td> 支持 </td></tr><tr><td> 可中断 </td><td> 不支持 </td><td> 支持 </td></tr><tr><td> 超时获取 </td><td> 不支持 </td><td> 支持 </td></tr><tr><td> 底层 </td><td> JVM </td><td> AQS </td></tr></tbody></table><h3 id="reentrantlock">ReentrantLock的使用场景</h3><p>一般场景我优先使用 synchronized，代码简单且 JVM 已做了大量优化；当需要超时获取锁、可中断、公平性或多个条件队列时，我会使用 ReentrantLock，它提供了更灵活、更可控的并发能力。</p></div></div>]]></description><link>https://binzi.top/posts/java/Synchronized%20%E4%B8%8E%20ReentrantLock%20%E7%9A%84%E5%8C%BA%E5%88%AB</link><guid isPermaLink="true">https://binzi.top/posts/java/Synchronized%20%E4%B8%8E%20ReentrantLock%20%E7%9A%84%E5%8C%BA%E5%88%AB</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Thu, 23 Jan 2025 13:28:00 GMT</pubDate></item><item><title><![CDATA[【题解】P1029]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1029%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1029%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1029httpswwwluogucomcnproblemp1029">P1029（<a href="https://www.luogu.com.cn/problem/P1029">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>输入两个正整数 $x<em>0, y</em>0$，求出满足下列条件的 $P, Q$ 的个数：</p><ol start="1"><li>$P,Q$ 是正整数。</li><li>要求 $P, Q$ 以 $x<em>0$ 为最大公约数，以 $y</em>0$ 为最小公倍数。</li></ol><p>试求：满足条件的所有可能的 $P, Q$ 的个数。</p><p>输入格式：</p><p>一行两个正整数 $x<em>0, y</em>0$。</p><p>输出格式：</p><p>一行一个数，表示求出满足条件的 $P, Q$ 的个数。</p><p>数据范围与说明：</p><p>$P,Q$ 有 $4$ 种：</p><ol start="1"><li>$3, 60$。</li><li>$15, 12$。</li><li>$12, 15$。</li><li>$60, 3$。</li></ol><p>对于 $100%$ 的数据，$2 \le x<em>0, y</em>0 \le {10}^5$。</p><p><strong>【题目来源】</strong></p><p>NOIP 2001 普及组第二题</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">3 60
</code></pre>
<p>输出：</p><pre class=""><code class="">4
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>输入两个正整数 $x<em>0, y</em>0$，求出满足下列条件的 $P, Q$ 的个数：</p><ol start="1"><li>$P,Q$ 是正整数。</li><li>要求 $P, Q$ 以 $x<em>0$ 为最大公约数，以 $y</em>0$ 为最小公倍数。</li></ol><p>试求：满足条件的所有可能的 $P, Q$ 的个数。</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
typedef long long ll;
ll gcd(int x,int y){
    while(x%y){
        int tmp=x%y;
        x=y;
        y=tmp;
    }
    return y;
}
ll lcm(int x,int y){
    return x*y/gcd(x,y);
}

int main(){
    ll x,y;
    cin&gt;&gt;x&gt;&gt;y;
    ll count=0;
     for (int i = x; i &lt;= y; i ++) { 
        int j = x * y / i;      
        if (gcd(i, j) == x &amp;&amp; lcm(i, j) == y) {
            count++;
        }
    }
    cout&lt;&lt;count;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1029%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1029%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Thu, 23 Jan 2025 10:39:13 GMT</pubDate></item><item><title><![CDATA[洛谷 P10389 蓝桥杯 2024 省赛成绩统计]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10389%E6%88%90%E7%BB%A9%E7%BB%9F%E8%AE%A1">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10389%E6%88%90%E7%BB%A9%E7%BB%9F%E8%AE%A1</a></blockquote><div><h1 id="-2024--ahttpswwwluogucomcnproblemp10389"><a href="https://www.luogu.com.cn/problem/P10389">蓝桥杯 2024 省赛 A：成绩统计</a></h1><h2 id="">题目描述</h2><p>有 $n$ 位同学，按照进教室的顺序依次得到编号 $1 \ldots n$，第 $i$ 位同学的成绩为 $a_i$。小蓝依次查看同学成绩：当他看完前 $x$ 位同学的成绩时，可以在这 $x$ 个人中任意挑出 $k$ 名同学，计算他们成绩的方差。请问至少需要查看多少位同学，才有可能找到一组方差严格小于阈值 $T$ 的 $k$ 人集合。如果始终无法满足条件，输出 $-1$。</p><p>方差 $\sigma^2$ 的定义为</p><p>$$\sigma^2 = \frac{1}{k}\sum<em>{i=1}^{k}(v</em>i-\bar v)^2,\quad\bar v = \frac{1}{k}\sum<em>{i=1}^{k}v</em>i.$$</p><h2 id="">输入格式</h2><ul><li>第一行：三个整数 $n,k,T$。</li><li>第二行：$n$ 个整数 $a<em>1,a</em>2,\ldots,a_n$。</li></ul><h2 id="">输出格式</h2><p>输出一个整数表示最少需要查看的同学数量；若无法找到满足条件的集合，输出 <code>-1</code>。</p><h2 id="">样例</h2><pre class=""><code class="">输入
5 3 1
3 2 5 2 3

输出
4
</code></pre>
<p>前三人只能选到 ${3,2,5}$，方差为约 $1.56$；观察到第四人后可以选 ${3,2,2}$，方差约 $0.22 &lt; 1$，因此答案为 4。</p><h2 id="">思路</h2><ul><li>对答案二分枚举 $x$，表示“只看前 $x$ 位同学”是否可行。</li><li>对前 $x$ 个成绩复制并排序，方差最小的 $k$ 人一定是排序后相邻的 $k$ 个。</li><li>使用前缀和维护滑动窗口内的和与平方和，快速计算每组 $k$ 个数的方差。</li><li>如果存在某组方差不超过阈值，则当前 $x$ 可行，继续收缩右端；否则扩大搜索范围。</li></ul><h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;

const int MAXN = 100005;
long long n, k, T;
long long a[MAXN], tmp[MAXN], prefix[MAXN], prefixSq[MAXN];

bool check(long long x) {
    for (int i = 1; i &lt;= x; ++i) tmp[i] = a[i];
    sort(tmp + 1, tmp + x + 1);
    prefix[0] = prefixSq[0] = 0;
    for (int i = 1; i &lt;= x; ++i) {
        prefix[i] = prefix[i - 1] + tmp[i];
        prefixSq[i] = prefixSq[i - 1] + tmp[i] * tmp[i];
    }
    auto variance = [&amp;](int l, int r) {
        long long sum = prefix[r] - prefix[l - 1];
        long long sumSq = prefixSq[r] - prefixSq[l - 1];
        long double avg = (long double)sum / k;
        long double var = (sumSq - 2 * avg * sum + k * avg * avg) / k;
        return var;
    };
    for (int i = k; i &lt;= x; ++i) {
        if (variance(i - k + 1, i) &lt;= T) return true;
    }
    return false;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin &gt;&gt; n &gt;&gt; k &gt;&gt; T;
    for (int i = 1; i &lt;= n; ++i) cin &gt;&gt; a[i];

    long long l = k, r = n, ans = -1;
    while (l &lt;= r) {
        long long mid = (l + r) / 2;
        if (check(mid)) {
            ans = mid;
            r = mid - 1;
        } else {
            l = mid + 1;
        }
    }
    cout &lt;&lt; ans &lt;&lt; &#x27;\n&#x27;;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10389%E6%88%90%E7%BB%A9%E7%BB%9F%E8%AE%A1</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10389%E6%88%90%E7%BB%A9%E7%BB%9F%E8%AE%A1</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 20 Jan 2025 02:00:00 GMT</pubDate></item><item><title><![CDATA[【题解】P3918]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P3918%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P3918%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p3918">P3918</h1><h2 id="">题目</h2><p>题目描述：</p><p>神犇航空开展了一项载客特技飞行业务。每次飞行长 $n$ 个单位时间，每个单位时间可以进行一项特技动作，可选的动作有 $k$ 种，每种动作有一个刺激程度 $c_i$。如果连续进行相同的动作，乘客会感到厌倦，所以定义某次动作的价值为(距上次该动作的时间) ，若为第一次进行该动作，价值为 $0$。安排一种方案，使得总价值最大。</p><p>输入格式：</p><p>第一行，两个整数，$n$ 和 $k$，如上所述；</p><p>第二行，$k$ 个整数，表示 $k$ 种动作的 $c_i$ 值。</p><p>输出格式：</p><p>仅一行，一个整数，表示最大总价值。</p><p>数据范围与说明：</p><h4 id="">数据规模与约定</h4><ul><li>对于 $10%$ 的测试数据，$n \le 20$，$k \le 3$。</li><li>对于 $100%$ 的测试数据，$1 \le n \le 10^3$，$1 \le k \le 300$，$0 \le c_i \le 10^3$。</li></ul><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5 2
2 2
</code></pre>
<p>输出：</p><pre class=""><code class="">12
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
int main() {
    int n, k;
    cin &gt;&gt; n &gt;&gt; k;
    vector&lt;int&gt; c(k);
    for (int i = 0; i &lt; k; i++) {
        cin &gt;&gt; c[i];
    }
    sort(c.begin(), c.end(), greater&lt;int&gt;());
    int left = 0, right = n - 1;
    int ans = 0;
    for (int i = 0; i &lt; k; i++) {
        if (left &gt; right) break;
        ans += (right - left) * c[i];
        left++;
        right--;
    }
    cout &lt;&lt; ans &lt;&lt; endl;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P3918%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P3918%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sun, 19 Jan 2025 19:02:28 GMT</pubDate></item><item><title><![CDATA[【题解】P1028]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1028%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1028%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1028httpswwwluogucomcnproblemp1028">P1028（<a href="https://www.luogu.com.cn/problem/P1028">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>给出正整数 $n$，要求按如下方式构造数列：</p><ol start="1"><li>只有一个数 $n$ 的数列是一个合法的数列。</li><li>在一个合法的数列的末尾加入一个正整数，但是这个正整数不能超过该数列最后一项的一半，可以得到一个新的合法数列。</li></ol><p>请你求出，一共有多少个合法的数列。两个合法数列 $a, b$ 不同当且仅当两数列长度不同或存在一个正整数 $i \leq |a|$，使得 $a<em>i \neq b</em>i$。</p><p>输入格式：</p><p>输入只有一行一个整数，表示 $n$。</p><p>输出格式：</p><p>输出一行一个整数，表示合法的数列个数。</p><p>数据范围与说明：</p><h3 id="-1-">样例 1 解释</h3><p>满足条件的数列为：</p><ul><li>$6$</li><li>$6, 1$</li><li>$6, 2$</li><li>$6, 3$</li><li>$6, 2, 1$</li><li>$6, 3, 1$</li></ul><h3 id="">数据规模与约定</h3><p>对于全部的测试点，保证 $1 \leq n \leq 10^3$。</p><h3 id="">说明</h3><p>本题数据来源是 NOIP 2001 普及组第一题，但是原题的题面描述和数据不符，故对题面进行了修改，使之符合数据。原题面如下，谨供参考：</p><blockquote><p>我们要求找出具有下列性质数的个数（包含输入的正整数 $n$）。</p><p>先输入一个正整数 $n$（$n \le 1000$），然后对此正整数按照如下方法进行处理：</p><ol start="1"><li>不作任何处理；</li><li>在它的左边拼接一个正整数，但该正整数不能超过原数，或者是上一个被拼接的数的一半；</li><li>加上数后，继续按此规则进行处理，直到不能再加正整数为止。</li></ol></blockquote>
<p>感谢 @<a href="https://binzi.top/user/120868">dbxxx</a> 对本题情况的反馈，原题面的问题见<a href="https://www.luogu.com.cn/discuss/526184">本贴</a>。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">6
</code></pre>
<p>输出：</p><pre class=""><code class="">6
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>给出正整数 $n$，要求按如下方式构造数列：</p><ol start="1"><li>只有一个数 $n$ 的数列是一个合法的数列。</li><li>在一个合法的数列的末尾加入一个正整数，但是这个正整数不能超过该数列最后一项的一半，可以得到一个新的合法数列。</li></ol><p>请你求出，一共有多少个合法的数列。两个合法数列 $a, b$ 不同当且仅当两数列长度不同或存在一个正整数 $i \leq |a|$，使得 $a<em>i \neq b</em>i$。</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int n,f[1005];

int main(){
    cin&gt;&gt;n;
    f[0]=f[1]=1;
    for(int i=2;i&lt;=n;i++){
        if(i%2==0){
            f[i]=f[i-1]+f[i/2];
        }else{
            f[i]=f[i-1];
        }
    }
    cout&lt;&lt;f[n];
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1028%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1028%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 18 Jan 2025 12:05:24 GMT</pubDate></item><item><title><![CDATA[【题解】P10389]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10389%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10389%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p10389httpswwwluogucomcnproblemp10389">P10389（<a href="https://www.luogu.com.cn/problem/P10389">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>小蓝的班上有 $n$ 个人，一次考试之后小蓝想统计同学们的成绩，第 $i$ 名同学的成绩为 $a<em>i$。当小蓝统计完前 $x$ 名同学的成绩后，他可以从 $1 \sim x$ 中选出任意 $k$ 名同学的成绩，计算出这 $k$ 个成绩的方差。小蓝至少要检查多少个人的成<br/>绩，才有可能选出 $k$ 名同学，他们的方差小于一个给定的值 $T$？<br/>提示：$k$ 个数 $v</em>1, v<em>2, \cdots , v</em>k$ 的方差 $\sigma^2$ 定义为：$\sigma^2=\dfrac  {\sum<em>{i=1}^k(v</em>i-\bar v)^2} k$，其中 $\bar v$ 表示<br/>$v<em>i$ 的平均值，$\bar v = \dfrac {\sum</em>{i=1}^k v_i} k$。</p><p>输入格式：</p><p>输入的第一行包含三个正整数 ，相邻整数之间使用一个空格分隔。</p><p>第二行包含 $n$ 个正整数 $a<em>1, a2, \cdots, a</em>n$ ，相邻整数之间使用一个空格分隔。</p><p>输出格式：</p><p>输出一行包含一个整数表示答案。如果不能满足条件，输出 $-1$ 。</p><p>数据范围与说明：</p><p>检查完前三名同学的成绩后，只能选出 ，方差为 ；</p><p>检查完前四名同学的成绩后，可以选出 ，方差为 ，所以答案为 。</p><p>对于 $10%$ 的评测用例，保证 $1 ≤ n, k ≤ 10^2$；<br/>对于 $30%$ 的评测用例，保证 $1 ≤ n, k ≤ 10^3$ ；<br/>对于所有评测用例，保证 ，，。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5 3 1
3 2 5 2 3
</code></pre>
<p>输出：</p><pre class=""><code class="">4
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>小蓝的班上有 $n$ 个人，一次考试之后小蓝想统计同学们的成绩，第 $i$ 名同学的成绩为 $a<em>i$。当小蓝统计完前 $x$ 名同学的成绩后，他可以从 $1 \sim x$ 中选出任意 $k$ 名同学的成绩，计算出这 $k$ 个成绩的方差。小蓝至少要检查多少个人的成<br/>绩，才有可能选出 $k$ 名同学，他们的方差小于一个给定的值 $T$？<br/>提示：$k$ 个数 $v</em>1, v<em>2, \cdots , v</em>k$ 的方差 $\sigma^2$ 定义为：$\sigma^2=\dfrac  {\sum<em>{i=1}^k(v</em>i-\bar v)^2} k$，其中 $\bar v$ 表示<br/>$v<em>i$ 的平均值，$\bar v = \dfrac {\sum</em>{i=1}^k v_i} k$。</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
typedef long long LL;
typedef double db;
LL n,k,T,a[100005],s[100005],qsum[100005],qpf[100005];
bool check(LL x){
    for(int i=1;i&lt;=x;i++)   s[i]=a[i];
    sort(s+1,s+x+1);
    qsum[0]=0,qpf[0]=0;
    for(int i=1;i&lt;=x;i++)   qsum[i]=qsum[i-1]+s[i];
    for(int i=1;i&lt;=x;i++)   qpf[i]=qpf[i-1]+s[i]*s[i];
    db fc=0,avg=0;
    for(int i=1;i&lt;=k;i++) avg+=(db)s[i]/k;
    fc=(qpf[k]-2*avg*qsum[k]+k*avg*avg)/k;
    if(T&gt;=fc){
        return true;
    }
    for(int i=k+1;i&lt;=x;i++){
        avg=avg-(db)s[i-k]/k+(db)s[i]/k;
        fc=(qpf[i]-qpf[i-k]-2*avg*(qsum[i]-qsum[i-k])+k*avg*avg)/k;
        if(T&gt;=fc){
            return true;
        }
    }
    return false;
}

int main(){
    cin&gt;&gt;n&gt;&gt;k&gt;&gt;T;
    for(int i=1;i&lt;=n;i++)   cin&gt;&gt;a[i];
    LL l=k,r=n;
    LL ans=n+1;
    while(l&lt;=r){
        int mid=l+(r-l)/2;
        if(check(mid)){
            ans=mid;
            r=mid-1;
        }else{
            l=mid+1;
        }
    }
    if(ans&gt;n) cout&lt;&lt;-1;
    else cout&lt;&lt;ans;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10389%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10389%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Thu, 16 Jan 2025 14:17:43 GMT</pubDate></item><item><title><![CDATA[【题解】P1115 最大子段和]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1115%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1115%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1115-httpswwwluogucomcnproblemp1115">P1115 最大子段和（<a href="https://www.luogu.com.cn/problem/P1115">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>给出一个长度为 $n$ 的序列 $a$，选出其中连续且非空的一段使得这段和最大。</p><p>输入格式：</p><p>第一行是一个整数，表示序列的长度 $n$。</p><p>第二行有 $n$ 个整数，第 $i$ 个整数表示序列的第 $i$ 个数字 $a_i$。</p><p>输出格式：</p><p>输出一行一个整数表示答案。</p><p>数据范围与说明：</p><h4 id="-1-">样例 1 解释</h4><p>选取 $[3, 5]$ 子段 ${3, -1, 2}$，其和为 $4$。</p><h4 id="">数据规模与约定</h4><ul><li>对于 $40%$ 的数据，保证 $n \leq 2 \times 10^3$。</li><li>对于 $100%$ 的数据，保证 $1 \leq n \leq 2 \times 10^5$，$-10^4 \leq a_i \leq 10^4$。</li></ul><hr/><p>2026/01/21：增加一组 <a href="https://www.luogu.com.cn/ticket/KLET103093">hack 数据</a>。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">7
2 -4 3 -1 2 -4 3
</code></pre>
<p>输出：</p><pre class=""><code class="">4
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int sum[200005],a[200005];
int main(){
    int n,ans=-2147483647;
    cin&gt;&gt;n;
    for(int i=1;i&lt;=n;i++){
        cin&gt;&gt;a[i];
        sum[i]=sum[i-1]+a[i];
        sum[i]=max(a[i],sum[i]);
        ans=max(ans,sum[i]);
    }
    cout&lt;&lt;ans;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1115%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1115%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 15 Jan 2025 09:35:01 GMT</pubDate></item><item><title><![CDATA[【题解】P1271]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1271%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1271%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1271">P1271</h1><h2 id="">题目</h2><p>题目描述：</p><p>学校正在选举学生会成员，有 $n$（$1 \le n\le 999$）名候选人，每名候选人编号分别从 $1$ 到 $n$，现在收集到了 $m$（$1 \le m \le 2000000$）张选票，每张选票都写了一个候选人编号。现在想把这些堆积如山的选票按照投票数字从小到大排序。设第 $i$（$1 \le i \le m$）张选票上的数字为 $a<em>i$，则保证有 $1 \le a</em>i \le n$。</p><p>输入格式：</p><p>输入 $n$ 和 $m$ 以及 $m$ 个选票上的数字。</p><p>输出格式：</p><p>求出排序后的选票编号。</p><p>数据范围与说明：</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5 10
2 5 2 2 5 2 2 2 1 2
</code></pre>
<p>输出：</p><pre class=""><code class="">1 2 2 2 2 2 2 2 5 5
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
int n,m;
int main(){
    cin&gt;&gt;n&gt;&gt;m;
    vector&lt;int&gt;a(m);
    for(int i=0;i&lt;m;i++)    cin&gt;&gt;a[i];
    sort(a.begin(),a.end());
    for(int i=0;i&lt;m;i++)    cout&lt;&lt;a[i]&lt;&lt;&quot; &quot;;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1271%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1271%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 14 Jan 2025 14:06:32 GMT</pubDate></item><item><title><![CDATA[【题解】P1806]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1806%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1806%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1806">P1806</h1><h2 id="">题目</h2><p>题目描述：</p><p>路人甲准备跑 $n$ 圈来锻炼自己的身体，他准备分多次（$\gt1$）跑完，每次都跑正整数圈，然后休息下再继续跑。</p><p>为了有效地提高自己的体能，他决定每次跑的圈数都必须比上次跑的多。</p><p>可以假设他刚开始跑了 $0$ 圈，那么请问他可以有多少种跑完这 $n$ 圈的方案？</p><p>输入格式：</p><p>一行一个整数，代表 $n$。</p><p>输出格式：</p><p>一个整数表示跑完这 $n$ 圈的方案数。</p><p>数据范围与说明：</p><h4 id="">数据规模与约定</h4><p>对于 $100%$ 的数据，保证 $5\le n\le 500$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">212
</code></pre>
<p>输出：</p><pre class=""><code class="">995645335
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
int n;
long long dp[505];
int main(){
    cin&gt;&gt;n;
    dp[0]=1;
    for(int i=1;i&lt;=n;i++){
        for(int j=n;j&gt;=i;j--){
            dp[j]+=dp[j-i];
        }
    }
    cout&lt;&lt;dp[n]-1;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1806%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1806%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 14 Jan 2025 12:23:12 GMT</pubDate></item><item><title><![CDATA[【题解】P1419]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1419%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1419%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1419">P1419</h1><h2 id="">题目</h2><p>题目描述：</p><p>给定一个长度为 $n$ 的序列 $a$，定义 $a_i$ 为第 $i$ 个元素的价值。现在需要找出序列中最有价值的“段落”。段落的定义是长度在 $[S, T]$ 之间的连续序列。最有价值段落是指平均值最大的段落。</p><p><strong>段落的平均值</strong> 等于 <strong>段落总价值</strong> 除以 <strong>段落长度</strong>。</p><p>输入格式：</p><p>第一行一个整数 $n$，表示序列长度。</p><p>第二行两个整数 $S$ 和 $T$，表示段落长度的范围，在 $[S, T]$ 之间。</p><p>第三行到第 $n+2$ 行，每行一个整数表示每个元素的价值指数。</p><p>输出格式：</p><p>一个实数，保留 $3$ 位小数，表示最优段落的平均值。</p><p>数据范围与说明：</p><p>【数据范围】</p><p>对于 $30%$ 的数据有 $n \le 1000$。</p><p>对于 $100%$ 的数据有 $1 \le n \le 100000$，$1 \le S \le T \le n$，$-{10}^4 \le a_i \le {10}^4$。</p><p>【题目来源】</p><p>tinylic 改编</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">3
2 2
3
-1
2
</code></pre>
<p>输出：</p><pre class=""><code class="">1.000
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int a[100005];
double b[100005],sum[100005];
int n;
int s,t;
bool check(double x){
    sum[0]=0;
    deque&lt;int&gt; q;
    for(int i=1;i&lt;=n;i++){
        b[i]=(double)a[i]-x;
        sum[i]=sum[i-1]+b[i];
    }
    for(int i=1;i&lt;=n;i++){
        if(i&gt;=s){
            while(!q.empty()&amp;&amp;sum[q.back()]&gt;sum[i-s]){
                q.pop_back();
            }
            q.push_back(i-s);   
        }
        while(!q.empty()&amp;&amp;q.front()&lt;i-t){
            q.pop_front();
        }
        if(!q.empty()&amp;&amp;sum[i]-sum[q.front()]&gt;=0){
             return true;
        }
    }
    return false;
}
int main(){
    cin&gt;&gt;n;
    cin&gt;&gt;s&gt;&gt;t;    
    for(int i=1;i&lt;=n;i++){
        cin&gt;&gt;a[i];
    }
    double ans;
    double l=-10000,r=10000;
    while(r-l&gt;1e-5){
       double mid=l+(r-l)/2;
        if(check(mid)){
            ans=mid;
            l=mid;
        }else{
            r=mid;
        }
    }
    printf(&quot;%.3lf&quot;,ans);
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1419%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1419%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 13 Jan 2025 15:29:07 GMT</pubDate></item><item><title><![CDATA[【题解】P10903]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10903%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10903%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p10903httpswwwluogucomcnproblemp10903">P10903（<a href="https://www.luogu.com.cn/problem/P10903">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>在库存管理系统中，跟踪和调节商品库存量是关键任务之一。小蓝经营的仓库中存有多种商品，这些商品根据类别和规格被有序地分类并编号，编号范围从 $1$ 至 $n$。初始时，每种商品的库存量均为 $0$。</p><p>为了高效地监控和调整库存量，小蓝的管理团队设计了 $m$ 个操作，每个操作涉及到一个特定的商品区间，即一段连续的商品编号范围（例如区间 $[L, R]$）。执行这些操作时，区间内每种商品的库存量都将增加 $1$。然而，在某些情况下，管理团队可能会决定不执行某些操作，使得这些操作涉及的商品区间内的库存量不会发生改变，维持原有的状态。</p><p>现在，管理团队需要一个评估机制，来确定如果某个操作未被执行，那么最终会有多少种商品的库存量为 $0$。对此，请你为管理团队计算出，对于每个操作，如果不执行该操作而执行其它操作，库存量为 $0$ 的商品的种类数。</p><p>输入格式：</p><p>输入的第一行包含两个整数 $n$ 和 $m$，分别表示商品的种类数和操作的个数。</p><p>接下来的 $m$ 行，每行包含两个整数 $L$ 和 $R$，表示一个操作涉及的商品区间。</p><p>输出格式：</p><p>输出 $m$ 行，每行一个整数，第 $i$ 行的整数表示如果不执行第 $i$ 个操作，则最终库存量为 $0$ 的商品种类数。</p><p>数据范围与说明：</p><p><strong>【样例说明】</strong></p><p>考虑不执行每个操作时，其余操作对商品库存的综合影响：</p><ul><li><strong>不执行操作 $1$</strong>：剩余的操作是操作 $2$（影响区间 $[2, 4]$）和操作 $3$（影响区间 $[3, 5]$）。执行这两个操作后，商品库存序列变为 $[0, 1, 2, 2, 1]$。在这种情况下，只有编号为 $1$ 的商品的库存量为 $0$。因此，库存量为 $0$ 的商品种类数为 $1$。</li><li><strong>不执行操作 $2$</strong>：剩余的操作是操作 $1$（影响区间 $[1, 2]$）和操作 $3$（影响区间 $[3, 5]$）。执行这两个操作后，商品库存序列变为 $[1, 1, 1, 1, 1]$。在这种情况下，所有商品的库存量都不为 $0$。因此，库存量为 $0$ 的商品种类数为 $0$。</li><li><strong>不执行操作 $3$</strong>：剩余的操作是操作 $1$（影响区间 $[1, 2]$）和操作 $2$（影响区间 $[2, 4]$）。执行这两个操作后，商品库存序列变为 $[1, 2, 1, 1, 0]$。在这种情况下，只有编号为 $5$ 的商品的库存量为 $0$。因此，库存量为 $0$ 的商品种类数为 $1$。</li></ul><p><strong>【评测用例规模与约定】</strong></p><p>对于 $20%$ 的评测用例，$1 \le n,m \le 5 \times 10^3$，$1\le L \le R \le n$。<br/>对于所有评测用例，$1 \le n,m \le 3 \times 10^5$，$1 \le L \le R \le n$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5 3
1 2
2 4
3 5
</code></pre>
<p>输出：</p><pre class=""><code class="">1
0
1
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>在库存管理系统中，跟踪和调节商品库存量是关键任务之一。小蓝经营的仓库中存有多种商品，这些商品根据类别和规格被有序地分类并编号，编号范围从 $1$ 至 $n$。初始时，每种商品的库存量均为 $0$。</p><p>为了高效地监控和调整库存量，小蓝的管理团队设计了 $m$ 个操作，每个操作涉及到一个特定的商品区间，即一段连续的商品编号范围（例如区间 $[L, R]$）。执行这些操作时，区间内每种商品的库存量都将增加 $1$。然而，在某些情况下，管理团队可能会决定不执行某些操作，使得这些操作涉及的商品区间内的库存量不会发生改变，维持原有的状态。</p><p>现在，管理团队需要一个评估机制，来确定如果某个操作未被执行，那么最终会有多少种商品的库存量为 $0$。对此，请你为管理团队计算出，对于每个操作，如果不执行该操作而执行其它操作，库存量为 $0$ 的商品的种类数。</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int l[300005],r[300005],a[300005],s[300005],sum=0;
int main(){
    int n,m;
    cin&gt;&gt;n&gt;&gt;m;
    for(int i=1;i&lt;=m;++i){
        cin&gt;&gt;l[i]&gt;&gt;r[i];
        a[l[i]]++;
        a[r[i]+1]--;
    }
    for(int i=1;i&lt;=n;++i)
{
a[i]+=a[i-1];
if(a[i]==0) sum++;
s[i]=s[i-1]+(a[i]==1);
}
    for(int i=1;i&lt;=m;++i) 
        cout&lt;&lt;s[r[i]]-s[l[i]-1]+sum&lt;&lt;endl;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10903%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10903%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 11 Jan 2025 12:56:29 GMT</pubDate></item><item><title><![CDATA[【题解】P2249 【深基13.例1】查找]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2249%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2249%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p2249-131httpswwwluogucomcnproblemp2249">P2249 【深基13.例1】查找（<a href="https://www.luogu.com.cn/problem/P2249">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>输入 $n$ 个不超过 $10^9$ 的单调不减的（就是后面的数字不小于前面的数字）非负整数 $a<em>1,a</em>2,\dots,a_{n}$，然后进行 $m$ 次询问。对于每次询问，给出一个整数 $q$，要求输出这个数字在序列中第一次出现的编号，如果没有找到的话输出 $-1$ 。</p><p>输入格式：</p><p>第一行 $2$ 个整数 $n$ 和 $m$，表示数字个数和询问次数。</p><p>第二行 $n$ 个整数，表示这些待查询的数字。</p><p>第三行 $m$ 个整数，表示询问这些数字的编号，从 $1$ 开始编号。</p><p>输出格式：</p><p>输出一行，$m$ 个整数，以空格隔开，表示答案。</p><p>数据范围与说明：</p><p>数据保证，$1 \leq n \leq 10^6$，$0 \leq a_i,q \leq 10^9$，$1 \leq m \leq 10^5$</p><p>本题输入输出量较大，请使用较快的 IO 方式。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">11 3
1 3 3 3 5 7 9 11 13 15 15
1 3 6
</code></pre>
<p>输出：</p><pre class=""><code class="">1 2 -1
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
int main() {
    int n, m;
    scanf(&quot;%d %d&quot;, &amp;n, &amp;m);
    vector&lt;int&gt; arr(n);
    for (int i = 0; i &lt; n; i++) {
        scanf(&quot;%d&quot;, &amp;arr[i]);
    }
    while (m--) {
        int x;
        scanf(&quot;%d&quot;, &amp;x);
        auto it = lower_bound(arr.begin(), arr.end(), x);
        int idx = it - arr.begin();
        if (it == arr.end() || *it != x) {
            printf(&quot;-1 &quot;);
        } else {
            printf(&quot;%d &quot;, idx+1);
        }
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2249%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2249%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 31 Dec 2024 14:20:33 GMT</pubDate></item><item><title><![CDATA[【题解】P1873]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1873%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1873%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1873">P1873</h1><h2 id="">题目</h2><p>题目描述：</p><p>伐木工人 Mirko 需要砍 $M$ 米长的木材。对 Mirko 来说这是很简单的工作，因为他有一个漂亮的新伐木机，可以如野火一般砍伐森林。不过，Mirko 只被允许砍伐一排树。</p><p>Mirko 的伐木机工作流程如下：Mirko 设置一个高度参数 $H$（米），伐木机升起一个巨大的锯片到高度 $H$，并锯掉所有树比 $H$ 高的部分（当然，树木不高于 $H$ 米的部分保持不变）。Mirko 就得到树木被锯下的部分。例如，如果一排树的高度分别为 $20,15,10$ 和 $17$，Mirko 把锯片升到 $15$ 米的高度，切割后树木剩下的高度将是 $15,15,10$ 和 $15$，而 Mirko 将从第 $1$ 棵树得到 $5$ 米，从第 $4$ 棵树得到 $2$ 米，共得到 $7$ 米木材。</p><p>Mirko 非常关注生态保护，所以他不会砍掉过多的木材。这也是他尽可能高地设定伐木机锯片的原因。请帮助 Mirko 找到伐木机锯片的最大的整数高度 $H$，使得他能得到的木材至少为 $M$ 米。换句话说，如果再升高 $1$ 米，他将得不到 $M$ 米木材。</p><p>输入格式：</p><p>第 $1$ 行 $2$ 个整数 $N$ 和 $M$，$N$ 表示树木的数量，$M$ 表示需要的木材总长度。</p><p>第 $2$ 行 $N$ 个整数表示每棵树的高度。</p><p>输出格式：</p><p>$1$ 个整数，表示锯片的最高高度。</p><p>数据范围与说明：</p><p>对于 $100%$ 的测试数据，$1\le N\le10^6$，$1\le M\le2\times10^9$，树的高度 $\le 4\times 10^5$，所有树的高度总和 $&gt;M$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">4 7
20 15 10 17
</code></pre>
<p>输出：</p><pre class=""><code class="">15
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">5 20
4 42 40 26 46
</code></pre>
<p>输出：</p><pre class=""><code class="">36
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
typedef long long ll;
int n,m;
int ans;
ll check(vector&lt;int&gt; arr,int mid){
    ll sum=0;
    for(int x:arr){
        if(x&gt;mid){
            sum+=x-mid;
        }
    }
    return sum;
}
int main(){
    cin&gt;&gt;n&gt;&gt;m;
    vector&lt;int&gt;arr(n);
    for(ll i=0;i&lt;n;i++){
        cin&gt;&gt;arr[i];
    }
    sort(arr.begin(),arr.end());
    int left=0,right=arr[n-1];
    while(left&lt;=right){
        int mid=left+(right-left)/2;
        ll sum=check(arr,mid);
        if(sum&gt;=m){
            ans=mid;
            left=mid+1;
        }
        else{
            right=mid-1;
        }
    }
    cout&lt;&lt;ans;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1873%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1873%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 30 Dec 2024 12:14:57 GMT</pubDate></item><item><title><![CDATA[【题解】P10424]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10424%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10424%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p10424httpswwwluogucomcnproblemp10424">P10424（<a href="https://www.luogu.com.cn/problem/P10424">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>一个整数如果按从低位到高位的顺序，奇数位（个位、百位、万位……）上的数字是奇数，偶数位（十位、千位、十万位……）上的数字是偶数，我们就称之为“好数”。</p><p>给定一个正整数 $N$，请计算从 $1$ 到 $N$ 一共有多少个好数。</p><p>输入格式：</p><p>一个整数 $N$。</p><p>输出格式：</p><p>一个整数代表答案。</p><p>数据范围与说明：</p><h3 id="-1-">样例 1 解释</h3><p>$24$ 以内的好数有 $1,3,5,7,9,21,23$，一共 $7$ 个。</p><h3 id="">数据规模与约定</h3><ul><li>对于 $10%$ 的测试数据，$1 \leq N \le 100$。</li><li>对于全部的测试数据，$1 \le N \leq 10^7$。</li></ul><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">24
</code></pre>
<p>输出：</p><pre class=""><code class="">7
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">2024
</code></pre>
<p>输出：</p><pre class=""><code class="">150
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>一个整数如果按从低位到高位的顺序，奇数位（个位、百位、万位……）上的数字是奇数，偶数位（十位、千位、十万位……）上的数字是偶数，我们就称之为“好数”。</p><p>给定一个正整数 $N$，请计算从 $1$ 到 $N$ 一共有多少个好数。</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
bool check(int x){
    int j=1;
    while(x){
        int i=x%10;
        if(j%2==1){
            if(i%2==0)  return false;
        }else{
            if(i%2==1)  return false;
        }
        
        x/=10;
        j++;
    }
    return true;
}

int main(){
    int n,count=0;
    cin&gt;&gt;n;
    for(int i=1;i&lt;=n;i++){
        if(check(i)){
            count++;
        }
    }
    cout&lt;&lt;count;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10424%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10424%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sun, 29 Dec 2024 14:20:53 GMT</pubDate></item><item><title><![CDATA[【题解】P1017 [NOIP 2000 提高组] 进制转换]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1017%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1017%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1017-noip-2000--httpswwwluogucomcnproblemp1017">P1017 [NOIP 2000 提高组] 进制转换（<a href="https://www.luogu.com.cn/problem/P1017">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>我们可以用这样的方式来表示一个十进制数：将每个阿拉伯数字乘以一个以该数字所处位置为指数，以 $10$ 为底数的幂之和的形式。例如 $123$ 可表示为 $1 \times 10^2+2\times 10^1+3\times 10^0$ 这样的形式。</p><p>与之相似的，对二进制数来说，也可表示成每个二进制数码乘以一个以该数字所处位置为指数，以 $2$ 为底数的幂之和的形式。</p><p>一般说来，任何一个正整数 $R$ 或一个负整数 $-R$ 都可以被选来作为一个数制系统的基数。如果是以 $R$ 或 $-R$ 为基数，则需要用到的数码为 $0,1,\dots,R-1$。</p><p>例如当 $R=7$ 时,所需用到的数码是 $0,1,2,3,4,5,6$，这与其是 $R$ 或 $-R$ 无关。如果作为基数的数绝对值超过 $10$，则为了表示这些数码，通常使用英文字母来表示那些大于 $9$ 的数码。例如对 $16$ 进制数来说,用 $A$ 表示 $10$，用 $B$ 表示 $11$，用 $C$ 表示 $12$，以此类推。</p><p>在负进制数中是用  作为基数，例如 $-15$（十进制）相当于 $(110001)_{-2}$（$-2$进制），并且它可以被表示为 $2$ 的幂级数的和数：</p><p>$$(110001)_{-2}=1\times (-2)^5+1\times (-2)^4+0\times (-2)^3+0\times (-2)^2+0\times (-2)^1 +1\times (-2)^0$$</p><p>设计一个程序，读入一个十进制数和一个负进制数的基数，并将此十进制数转换为此负进制下的数。</p><p>输入格式：</p><p>输入的每行有两个输入数据。</p><p>第一个是十进制数 $n$。第二个是负进制数的基数 $R$。</p><p>输出格式：</p><p>输出此负进制数及其基数，若此基数的绝对值超过 $10$，则参照 $16$ 进制的方式处理。</p><p>数据范围与说明：</p><p><strong>数据范围</strong></p><p>对于 $100%$ 的数据，$-20 \le R \le -2$，$|n| \le 37336$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">30000 -2
</code></pre>
<p>输出：</p><pre class=""><code class="">30000=11011010101110000(base-2)
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">-20000 -2
</code></pre>
<p>输出：</p><pre class=""><code class="">-20000=1111011000100000(base-2)
</code></pre>
<p>输入输出样例 #3</p><p>输入：</p><pre class=""><code class="">28800 -16
</code></pre>
<p>输出：</p><pre class=""><code class="">28800=19180(base-16)
</code></pre>
<p>输入输出样例 #4</p><p>输入：</p><pre class=""><code class="">-25000 -16
</code></pre>
<p>输出：</p><pre class=""><code class="">-25000=7FB8(base-16)
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>我们可以用这样的方式来表示一个十进制数：将每个阿拉伯数字乘以一个以该数字所处位置为指数，以 $10$ 为底数的幂之和的形式。例如 $123$ 可表示为 $1 \times 10^2+2\times 10^1+3\times 10^0$ 这样的形式。</p><p>与之相似的，对二进制数来说，也可表示成每个二进制数码乘以一个以该数字所处位置为指数，以 $2$ 为底数的幂之和的形式。</p><p>一般说来，任何一个正整数 $R$ 或一个负整数 $-R$ 都可以被选来作为一个数制系统的基数。如果是以 $R$ 或 $-R$ 为基数，则需要用到的数码为 $0,1,\dots,R-1$。</p><p>例如当 $R=7$ 时,所需用到的数码是 $0,1,2,3,4,5,6$，这与其是 $R$ 或 $-R$ 无关。如果作为基数的数绝对值超过 $10$，则为了表示这些数码，通常使用英文字母来表示那些大于 $9$ 的数码。例如对 $16$ 进制数来说,用 $A$ 表示 $10$，用 $B$ 表示 $11$，用 $C$ 表示 $12$，以此类推。</p><p>在负进制数中是用  作为基数，例如 $-15$（十进制）相当于 $(110001)_{-2}$（$-2$进制），并且它可以被表示为 $2$ 的幂级数的和数：</p><p>$$(110001)_{-2}=1\times (-2)^5+1\times (-2)^4+0\times (-2)^3+0\times (-2)^2+0\times (-2)^1 +1\times (-2)^0$$</p><p>设计一个程序，读入一个十进制数和一个负进制数的基数，并将此十进制数转换为此负进制下的数。</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int n,r;
int i=0;
char arr[100];
int main(){
    cin&gt;&gt;n&gt;&gt;r;
    
    int rr=r;
    printf(&quot;%d=&quot;,n);
    while(n!=0){
        int a=n%r;
        n/=r;
        if(a&lt;0){
            a-=r;
            n++;
        }
        if(a&lt;10){
            arr[i++]=(char)(a+48);
        }else{
            arr[i++]=(char)((a-10)+&#x27;A&#x27;);
        }
    }
    
    for(int z=i-1;z&gt;=0;z--){
        cout&lt;&lt;arr[z];
    }printf(&quot;(base%d)&quot;,rr);
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1017%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1017%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 28 Dec 2024 15:14:42 GMT</pubDate></item><item><title><![CDATA[【NOIP 2015 提高组】跳石头 - 题解]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2678%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2678%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="noip-2015-httpswwwluogucomcnproblemp2678">【NOIP 2015 提高组】跳石头（<a href="https://www.luogu.com.cn/problem/P2678">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>一年一度的“跳石头”比赛又要开始了！</p><p>这项比赛将在一条笔直的河道中进行，河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间，有 $N$ 块岩石（不含起点和终点的岩石）。在比赛过程中，选手们将从起点出发，每一步跳向相邻的岩石，直至到达终点。</p><p>为了提高比赛难度，组委会计划移走一些岩石，使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制，组委会至多从起点和终点之间移走 $M$ 块岩石（不能移走起点和终点的岩石）。</p><p>输入格式：</p><p>第一行包含三个整数 $L,N,M$，分别表示起点到终点的距离，起点和终点之间的岩石数，以及组委会至多移走的岩石数。保证 $L \geq 1$ 且 $N \geq M \geq 0$。</p><p>接下来 $N$ 行，每行一个整数，第 $i$ 行的整数 $D<em>i,( 0 &lt; D</em>i &lt; L)$， 表示第 $i$ 块岩石与起点的距离。这些岩石按与起点距离从小到大的顺序给出，且不会有两个岩石出现在同一个位置。</p><p>输出格式：</p><p>一个整数，即最短跳跃距离的最大值。</p><p>数据范围与说明：</p><h3 id="-1-">输入输出样例 1 说明</h3><p>将与起点距离为 $2$ 和 $14$ 的两个岩石移走后，最短的跳跃距离为 $4$（从与起点距离 $17$ 的岩石跳到距离 $21$ 的岩石，或者从距离 $21$ 的岩石跳到终点）。</p><h3 id="">数据规模与约定</h3><p>对于 $20%$的数据，$0 \le M \le N \le 10$。<br/>对于 $50%$ 的数据，$0 \le M \le N \le 100$。<br/>对于 $100%$ 的数据，$0 \le M \le N \le 50000,1 \le L  \le 10^9$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">25 5 2 
2
11
14
17 
21
</code></pre>
<p>输出：</p><pre class=""><code class="">4
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
int l,n,m,a[50006],ans;
bool check(int mid){
    int cnt=0,j=0;
    for(int i=1;i&lt;=n+1;i++){
        if(a[i]-a[j]&lt;mid){
            cnt++;
        }else{
            j=i;
        }
    }
    if(cnt&gt;m){
        return false;
    }else{
        return true;
    }
}
int main(){
    scanf(&quot;%d %d %d&quot;,&amp;l,&amp;n,&amp;m);
    for(int i=1;i&lt;=n;i++)   scanf(&quot;%d&quot;,&amp;a[i]);
    a[n+1]=l;
    int left=0,right=l;
    while(left&lt;=right)
     {
         int mid=left+(right-left)/2;
         if(check(mid)){
              left=mid+1;
              ans=mid;
          }
        else right=mid-1;
     }
    cout&lt;&lt;ans&lt;&lt;endl;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2678%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2678%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 27 Dec 2024 14:28:17 GMT</pubDate></item><item><title><![CDATA[【题解】P1824 [USACO05FEB] 进击的奶牛 Aggressive Cows]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1824%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1824%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1824-usaco05feb--aggressive-cowshttpswwwluogucomcnproblemp1824">P1824 [USACO05FEB] 进击的奶牛 Aggressive Cows（<a href="https://www.luogu.com.cn/problem/P1824">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>农夫约翰建造了一座有 $n$ 间牛舍的小屋，牛舍排在一条直线上，第 $i$ 间牛舍在 $x_i$ 的位置，但是约翰的 $m$ 头牛对小屋很不满意，因此经常互相攻击。约翰为了防止牛之间互相伤害，因此决定把每头牛都放在离其它牛尽可能远的牛舍。也就是要最大化最近的两头牛之间的距离。</p><p>牛们并不喜欢这种布局，而且几头牛放在一个隔间里，它们就要发生争斗。为了不让牛互相伤害。约翰决定自己给牛分配隔间，使任意两头牛之间的最小距离尽可能的大，那么，这个最大的最小距离是多少呢？</p><p>输入格式：</p><p>第一行用空格分隔的两个整数 $n$ 和 $m$；</p><p>下面 $n$ 行为 $n$ 个用空格隔开的整数，表示位置 $x_i$。</p><p>输出格式：</p><p>一行一个整数，表示最大的最小距离值。</p><p>数据范围与说明：</p><p>【样例解析】把牛放在 $1$，$4$，$8$ 这三个位置，距离是 $3$。容易证明最小距离已经最大。</p><p>【数据范围】对于 $100%$ 的数据，$2 \le n \le 10^5$，$0 \le x_i \le 10^9$，$2 \le m \le n$。不保证 $x$ 数组单调递增。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5 3
1
2
8
4
9
</code></pre>
<p>输出：</p><pre class=""><code class="">3
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int n,c,a[100005];
int ans;
bool check(int mid){
    int cnt=1,p=0;
    for(int i=1;i&lt;n;i++){
        if(a[i]-a[p]&gt;=mid){
            cnt++;
            p=i;
        }
    }
    if(cnt&gt;=c)  return true;
    else    return false;
}
int main(){
    scanf(&quot;%d %d&quot;,&amp;n,&amp;c);
    for(int i=0;i&lt;n;i++)    scanf(&quot;%d&quot;,&amp;a[i]);
    sort(a,a+n);
    int left=0,right=a[n-1]-a[0];
    while(left&lt;=right){
        int mid=left+(right-left)/2;
        if(check(mid)){
            ans=mid;
            left=mid+1;
        }else{
            right=mid-1;
        }
    }
    cout&lt;&lt;ans;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1824%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1824%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 25 Dec 2024 14:40:54 GMT</pubDate></item><item><title><![CDATA[【题解】P1102 A-B 数对]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1102%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1102%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1102-a-b-httpswwwluogucomcnproblemp1102">P1102 A-B 数对（<a href="https://www.luogu.com.cn/problem/P1102">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>给出一串正整数数列以及一个正整数 $C$，要求计算出所有满足 $A - B = C$ 的数对的个数（不同位置的数字一样的数对算不同的数对）。</p><p>输入格式：</p><p>输入共两行。</p><p>第一行，两个正整数 $N,C$。</p><p>第二行，$N$ 个正整数，作为要求处理的那串数。</p><p>输出格式：</p><p>一行，表示该串正整数中包含的满足 $A - B = C$ 的数对的个数。</p><p>数据范围与说明：</p><p>对于 $75%$ 的数据，$1 \leq N \leq 2000$。</p><p>对于 $100%$ 的数据，$1 \leq N \leq 2 \times 10^5$，$0 \leq a_i &lt;2^{30}$，$1 \leq C &lt; 2^{30}$。</p><p>2017/4/29 新添数据两组</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">4 1
1 1 2 3
</code></pre>
<p>输出：</p><pre class=""><code class="">3
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
typedef long long ll;
const int N=2e5+5;
int a[N];
map&lt;ll,ll&gt; m;
int main(){
    ll n,c;
    cin&gt;&gt;n&gt;&gt;c;
    for(int i=1;i&lt;=n;i++){
        cin&gt;&gt;a[i];
        m[a[i]]++;
        a[i]-=c;
    }
    ll ans=0;
    for(int i=1;i&lt;=n;i++)   ans+=m[a[i]];
    cout&lt;&lt;ans;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1102%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1102%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 25 Dec 2024 00:46:21 GMT</pubDate></item><item><title><![CDATA[【题解】P11395]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P11395%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P11395%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p11395">P11395</h1><h2 id="">题目</h2><p>题目描述：</p><p>喵喵喵群里面，吃喵喵是坏文明。</p><p>现在群友有 $q$ 个问题，每个问题形如 <code>A or B?</code>，其中 $A,B$ 为仅含小写字母的非空字符串。</p><p>对于每个问题，你可以回答 $A$ 或 $B$ 中的任意一个不是 <code>eat</code> 的字符串。特别地，如果 $A$ 和 $B$ 都是 <code>eat</code>，你需要回答 <code>or</code>。</p><p>输入格式：</p><p>输入的第一行有一个正整数 $q$，表示问题的数量。</p><p>之后有 $q$ 行，每行有一个问题。</p><p>输出格式：</p><p>对于每个问题，输出一行一个字符串，表示你对于这个问题的答案。</p><p>数据范围与说明：</p><p>【样例 1 解释】</p><ul><li>对于第一个问题，你可以从 <code>yummy</code> 和 <code>yucky</code> 两个单词中的任意一个，所以既可以回答 <code>yummy</code> 也可以回答 <code>yucky</code>。<strong>注意：在这个问题输出 <code>or</code> 将导致答案错误。</strong></li><li>对于第二个问题，因为第一个单词是 <code>eat</code>，要避开，因此<strong>必须</strong>输出 <code>stick</code>。</li><li>对于第三个问题，因为第二个单词是 <code>eat</code>，要避开，因此<strong>必须</strong>输出 <code>rua</code>。</li><li>对于第四个问题，两个单词都是 <code>eat</code>，所以要输出 <code>or</code>。</li></ul><p>【样例 2 解释】</p><p>本样例共有 $32$ 种本质不同的输出（除了第 $5,6$ 个问题的两个字符串一样外，其他问题都有两种可能答案），这里仅展示其中一种可能的输出。</p><p>【数据范围】</p><p>对于全部数据，保证 $1\le q\le 100$，每个问题中的字符串 $A,B$ 均不超过 $100$ 个字符，且仅含有小写字符。</p><p>本题共有 $6$ 个测试点，不同测试点的分值和特殊性质见下表：</p><table><thead><tr><th> 测试点编号 </th><th> 特殊性质 </th><th> 分值 </th></tr></thead><tbody><tr><td> 1 </td><td> $A,B$ 都不是 <code>eat</code> </td><td> $19$ </td></tr><tr><td> 2 </td><td> $A$ 一定是 <code>eat</code>，$B$ 一定不是 <code>eat</code> </td><td> $22$ </td></tr><tr><td> 3 </td><td> $B$ 一定是 <code>eat</code> </td><td> $11$ </td></tr><tr><td> 4 </td><td> $A$ 一定是 <code>eat</code> </td><td> $12$ </td></tr><tr><td> 5 </td><td> $A,B$ 一定有一个不是 <code>eat</code> </td><td> $13$ </td></tr><tr><td> 6 </td><td> 无特殊性质 </td><td> $23$ </td></tr></tbody></table><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">4
yummy or yucky?
eat or stick?
rua or eat?
eat or eat?
</code></pre>
<p>输出：</p><pre class=""><code class="">yummy
stick
rua
or
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">7
saint or sinner?
tobe or nottobe?
ad or able?
eating or eaten?
or or or?
and or and?
oneplusoneistwo or oneplusoneisthree?
</code></pre>
<p>输出：</p><pre class=""><code class="">sinner
tobe
able
eaten
or
and
oneplusoneistwo
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
string a,Or,b;
int main(){
    int q;
    for(cin&gt;&gt;q;q;q--){
        cin&gt;&gt;a&gt;&gt;Or&gt;&gt;b;
        b.pop_back();
        if(a==&quot;eat&quot;){
            if(b==&quot;eat&quot;){
                cout&lt;&lt;Or&lt;&lt;endl;
            }else{
                cout&lt;&lt;b&lt;&lt;endl;
            }
        }else{
            cout&lt;&lt;a&lt;&lt;endl;
        }
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P11395%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P11395%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 24 Dec 2024 14:02:43 GMT</pubDate></item><item><title><![CDATA[【模板】堆 - 题解]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P3378%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P3378%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="httpswwwluogucomcnproblemp3378">【模板】堆（<a href="https://www.luogu.com.cn/problem/P3378">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>给定一个数列，初始为空，请支持下面三种操作：</p><ol start="1"><li>给定一个整数 $x$，请将 $x$ 加入到数列中。</li><li>输出数列中最小的数。</li><li>删除数列中最小的数（如果有多个数最小，只删除 $1$ 个）。</li></ol><p>输入格式：</p><p>第一行是一个整数，表示操作的次数 $n$。<br/>接下来 $n$ 行，每行表示一次操作。每行首先有一个整数 $op$ 表示操作类型。</p><ul><li>若 $op = 1$，则后面有一个整数 $x$，表示要将 $x$ 加入数列。</li><li>若 $op = 2$，则表示要求输出数列中的最小数。</li><li>若 $op = 3$，则表示删除数列中的最小数。如果有多个数最小，只删除 $1$ 个。</li></ul><p>输出格式：</p><p>对于每个操作 $2$，输出一行一个整数表示答案。</p><p>数据范围与说明：</p><p><strong>【数据规模与约定】</strong></p><ul><li>对于 $30%$ 的数据，保证 $n \leq 15$。</li><li>对于 $70%$ 的数据，保证 $n \leq 10^4$。</li><li>对于 $100%$ 的数据，保证 $1 \leq n \leq 10^6$，$1 \leq x \lt 2^{31}$，$op \in {1, 2, 3}$。</li></ul><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5
1 2
1 5
2
3
2
</code></pre>
<p>输出：</p><pre class=""><code class="">2
5
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
priority_queue&lt;int,vector&lt;int&gt;,greater&lt;int&gt;&gt;q;
int main(){
    int n;
    scanf(&quot;%d&quot;,&amp;n);
    while(n--){
        int op;
        scanf(&quot;%d&quot;,&amp;op);
        if(op==1){
            int x;
            scanf(&quot;%d&quot;,&amp;x);
            q.push(x);
        }else if(op==2){
            printf(&quot;%d\n&quot;,q.top());
        }else{
            q.pop();
        }
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P3378%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P3378%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 23 Dec 2024 14:53:26 GMT</pubDate></item><item><title><![CDATA[【题解】P1229]]></title><description><![CDATA[<link rel="preload" as="image" href="https://cdn.luogu.com.cn/upload/image_hosting/w75s9yip.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1229%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1229%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1229">P1229</h1><h2 id="">题目</h2><p>题目描述：</p><p>我们都很熟悉二叉树的前序、中序、后序遍历，在数据结构中常提出这样的问题：已知一棵二叉树的前序和中序遍历，求它的后序遍历，相应的，已知一棵二叉树的后序遍历和中序遍历序列你也能求出它的前序遍历。然而给定一棵二叉树的前序和后序遍历，你却不能确定其中序遍历序列，考虑如下图中的几棵二叉树：</p><p><img src="https://cdn.luogu.com.cn/upload/image_hosting/w75s9yip.png" height="0" width="0"/></p><p>所有这些二叉树都有着相同的前序遍历和后序遍历，但中序遍历却不相同。</p><p>输入格式：</p><p>共两行，第一行表示该二叉树的前序遍历结果 $s<em>1$，第二行表示该二叉树的后序遍历结果 $s</em>2$。</p><p>保证至少存在一棵二叉树满足给出的信息，$s <em> 1, s </em> 2$ 中只含小写字母，且在某个字符串中不存在相同的字母。</p><p>输出格式：</p><p>输出可能的中序遍历序列的总数，结果不超过 $2^{63}-1$。</p><p>数据范围与说明：</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">abc                           
cba
</code></pre>
<p>输出：</p><pre class=""><code class="">4
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int main(){
    string a,b;
    int ans=0;
    cin&gt;&gt;a;
    cin&gt;&gt;b;
    for(int i=0;i&lt;a.length();i++){
        for(int j=1;j&lt;b.length();j++){
            if(a[i]==b[j]&amp;&amp;a[i+1]==b[j-1]){
                ans++;
            }
        }
    }
    cout&lt;&lt;(1&lt;&lt;ans);
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1229%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1229%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sun, 22 Dec 2024 12:14:25 GMT</pubDate></item><item><title><![CDATA[【题解】P1030]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1030%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1030%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1030httpswwwluogucomcnproblemp1030">P1030（<a href="https://www.luogu.com.cn/problem/P1030">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>给出一棵二叉树的中序与后序排列。求出它的先序排列。（约定树结点用不同的大写字母表示，且二叉树的节点个数 ）。</p><p>输入格式：</p><p>共两行，均为大写字母组成的字符串，表示一棵二叉树的中序与后序排列。</p><p>输出格式：</p><p>共一行一个字符串，表示一棵二叉树的先序。</p><p>数据范围与说明：</p><p><strong>【题目来源】</strong></p><p>NOIP 2001 普及组第三题</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">BADC
BDCA
</code></pre>
<p>输出：</p><pre class=""><code class="">ABCD
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>给出一棵二叉树的中序与后序排列。求出它的先序排列。（约定树结点用不同的大写字母表示，且二叉树的节点个数 ）。</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
string a,b;
void func(string x,string y){
    if(x.size()&gt;0){   
        char ch=y[y.size()-1];
        cout&lt;&lt;ch;
        int k=x.find(ch);
        func(x.substr(0,k),y.substr(0,k));
        func(x.substr(k+1),y.substr(k,x.size()-k-1));
    }

}
int main(){
    cin&gt;&gt;a;
    cin&gt;&gt;b;
    func(a,b);
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1030%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1030%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 21 Dec 2024 15:05:19 GMT</pubDate></item><item><title><![CDATA[【题解】P1087]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1087%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1087%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1087httpswwwluogucomcnproblemp1087">P1087（<a href="https://www.luogu.com.cn/problem/P1087">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>我们可以把由 0 和 1 组成的字符串分为三类：全 0 串称为 B 串，全 1 串称为 I 串，既含 0 又含 1 的串则称为 F 串。</p><p>FBI 树是一种二叉树，它的结点类型也包括 F 结点，B 结点和 I 结点三种。由一个长度为 $2^N$ 的 01 串 $S$ 可以构造出一棵 FBI 树 $T$，递归的构造方法如下：</p><ol start="1"><li>$T$ 的根结点为 $R$，其类型与串 $S$ 的类型相同；</li><li>若串 $S$ 的长度大于 $1$，将串 $S$ 从中间分开，分为等长的左右子串 $S<em>1$ 和 $S</em>2$；由左子串 $S<em>1$ 构造 $R$ 的左子树 $T</em>1$，由右子串 $S<em>2$ 构造 $R$ 的右子树 $T</em>2$。</li></ol><p>现在给定一个长度为 $2^N$ 的 01 串，请用上述构造方法构造出一棵 FBI 树，并输出它的后序遍历序列。</p><p>输入格式：</p><p>第一行是一个整数 $N(0 \le N \le 10)$，</p><p>第二行是一个长度为 $2^N$ 的 01 串。</p><p>输出格式：</p><p>一个字符串，即 FBI 树的后序遍历序列。</p><p>数据范围与说明：</p><p>对于 $40%$ 的数据，$N \le 2$；</p><p>对于全部的数据，$N \le 10$。</p><p>noip2004普及组第3题</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">3
10001011
</code></pre>
<p>输出：</p><pre class=""><code class="">IBFBBBFIBFIIIFF
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>我们可以把由 0 和 1 组成的字符串分为三类：全 0 串称为 B 串，全 1 串称为 I 串，既含 0 又含 1 的串则称为 F 串。</p><p>FBI 树是一种二叉树，它的结点类型也包括 F 结点，B 结点和 I 结点三种。由一个长度为 $2^N$ 的 01 串 $S$ 可以构造出一棵 FBI 树 $T$，递归的构造方法如下：</p><ol start="1"><li>$T$ 的根结点为 $R$，其类型与串 $S$ 的类型相同；</li><li>若串 $S$ 的长度大于 $1$，将串 $S$ 从中间分开，分为等长的左右子串 $S<em>1$ 和 $S</em>2$；由左子串 $S<em>1$ 构造 $R$ 的左子树 $T</em>1$，由右子串 $S<em>2$ 构造 $R$ 的右子树 $T</em>2$。</li></ol><p>现在给定一个长度为 $2^N$ 的 01 串，请用上述构造方法构造出一棵 FBI 树，并输出它的后序遍历序列。</p><h2 id="">代码</h2><h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
char func(string x){
    if(x.length()&gt;1){
        int mid=x.length()/2;
        cout&lt;&lt;func(x.substr(0,mid));
        cout&lt;&lt;func(x.substr(mid));
    }
    if(x==string(x.length(),&#x27;0&#x27;))   return &#x27;B&#x27;;
    if(x==string(x.length(),&#x27;1&#x27;))   return &#x27;I&#x27;;
    return &#x27;F&#x27;;
}

int main(){
    int n;
    cin&gt;&gt;n;
    n=1&gt;&gt;n;
    string s;
    cin&gt;&gt;s;
    cout&lt;&lt;func(s);

    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1087%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1087%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 20 Dec 2024 13:04:12 GMT</pubDate></item><item><title><![CDATA[【NOIP 2013 普及组】表达式求值 - 题解]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1981%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1981%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="noip-2013-httpswwwluogucomcnproblemp1981">【NOIP 2013 普及组】表达式求值（<a href="https://www.luogu.com.cn/problem/P1981">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>给定一个只包含加法和乘法的算术表达式，请你编程计算表达式的值。</p><p>输入格式：</p><p>一行，为需要你计算的表达式，表达式中只包含数字、加法运算符 <code>+</code> 和乘法运算符 <code>*</code>，且没有括号，所有参与运算的数字均为 $0$ 到 $2^{31}-1$ 之间的整数。</p><p>输入数据保证这一行只有 <code>0123456789+*</code> 这 $12$ 种字符。</p><p>输出格式：</p><p>一个整数，表示这个表达式的值。</p><p>注意：当答案长度多于 $4$ 位时，请只输出最后  位，前导  不输出。</p><p>数据范围与说明：</p><p>对于 $30%$ 的数据，$0≤$ 表达式中加法运算符和乘法运算符的总数 $≤100$。</p><p>对于 $80%$ 的数据，$0≤$ 表达式中加法运算符和乘法运算符的总数 $≤1000$。</p><p>对于 $100%$ 的数据，$0≤$ 表达式中加法运算符和乘法运算符的总数 $≤100000$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">1+1*3+4
</code></pre>
<p>输出：</p><pre class=""><code class="">8
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">1+1234567890*1
</code></pre>
<p>输出：</p><pre class=""><code class="">7891
</code></pre>
<p>输入输出样例 #3</p><p>输入：</p><pre class=""><code class="">1+1000000003*1
</code></pre>
<p>输出：</p><pre class=""><code class="">4
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
stack&lt;int&gt; s;
int main()
{
    char c;
    int a,b;
    int n=10000;
    a%=n;
    cin&gt;&gt;a;
    s.push(a);
    while(cin&gt;&gt;c&gt;&gt;b){
        if(c==&#x27;*&#x27;){
            a=s.top();
            s.pop();
            s.push(a*b%n);
        }else{
            s.push(b);
        }
    }
    int sum=0;
    while(!s.empty()){
        sum+=s.top();
        sum%=n;
        s.pop();
    }
    cout&lt;&lt;sum&lt;&lt;endl;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1981%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1981%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 18 Dec 2024 02:43:57 GMT</pubDate></item><item><title><![CDATA[【题解】P1739 表达式括号匹配]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1739%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1739%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1739-httpswwwluogucomcnproblemp1739">P1739 表达式括号匹配（<a href="https://www.luogu.com.cn/problem/P1739">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>假设一个表达式有英文字母（小写）、运算符（<code>+</code>、<code>-</code>、<code>*</code>、<code>/</code>）和左右小（圆）括号构成，以 <code>@</code> 作为表达式的结束符。请编写一个程序检查表达式中的左右圆括号是否匹配，若匹配，则输出 <code>YES</code>；否则输出 <code>NO</code>。表达式长度小于 $255$，左圆括号少于 $20$ 个。</p><p>输入格式：</p><p>一行：表达式。</p><p>输出格式：</p><p>一行：<code>YES</code> 或 <code>NO</code>。</p><p>数据范围与说明：</p><p>表达式长度小于 $255$，左圆括号少于 $20$ 个。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">2*(x+y)/(1-x)@
</code></pre>
<p>输出：</p><pre class=""><code class="">YES
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">(25+x)*(a*(a+b+b)@
</code></pre>
<p>输出：</p><pre class=""><code class="">NO
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
string s;
stack &lt;int&gt; stk;
int main(){
    int i=0;
    getline(cin,s);
    while(s[i]!=&#x27;@&#x27;)
    {
        if(stk.empty()){    
            if(s[i]==&#x27;)&#x27;){
                cout&lt;&lt;&quot;NO&quot;;
                return 0;
            }
        }
        if(s[i]==&#x27;(&#x27;){
            stk.push(s[i]);
        }
        if(s[i]==&#x27;)&#x27;){
            stk.pop();
        }
        i++;
    }
    if(stk.empty()){
        cout&lt;&lt;&quot;YES&quot;;
    }else {
        cout&lt;&lt;&quot;NO&quot;;
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1739%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1739%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 17 Dec 2024 14:26:50 GMT</pubDate></item><item><title><![CDATA[【题解】P1449 后缀表达式]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1449%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1449%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1449-httpswwwluogucomcnproblemp1449">P1449 后缀表达式（<a href="https://www.luogu.com.cn/problem/P1449">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>所谓后缀表达式是指这样的一个表达式：式中不再引用括号，运算符号放在两个运算对象之后，所有计算按运算符号出现的顺序，严格地由左而右新进行（不用考虑运算符的优先级）。</p><p>本题中运算符仅包含 $\texttt{+-<em>/}$。保证对于 $\texttt{/}$ 运算除数不为 0。特别地，其中 $\texttt{/}$ 运算的结果需要*</em>向 0 取整**（即与 C++ <code>/</code> 运算的规则一致）。</p><p>如：$\texttt{3<em>(5-2)+7}$ 对应的后缀表达式为：$\texttt{3.5.2.-</em>7.+@}$。在该式中，<code>@</code> 为表达式的结束符号。<code>.</code> 为操作数的结束符号。</p><p>输入格式：</p><p>输入一行一个字符串 $s$，表示后缀表达式。</p><p>输出格式：</p><p>输出一个整数，表示表达式的值。</p><p>数据范围与说明：</p><p>数据保证，$1 \leq |s| \leq 50$，答案和计算过程中的每一个值的绝对值不超过 $10^9$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">3.5.2.-*7.+@
</code></pre>
<p>输出：</p><pre class=""><code class="">16
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">10.28.30./*7.-@
</code></pre>
<p>输出：</p><pre class=""><code class="">-7
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
stack&lt;int&gt; num;
char s[10001];
char ccty[21];
int flag = 0, i = 0;
int numa, numb, Num;
int k = 0;
int makernum(){
int ret=0;
for(int i=0;i&lt;=strlen(ccty)-1;++i){
if(ccty[i]==&#x27;*&#x27;)break;
ret=ret*10+ccty[i]-&#x27;0&#x27;;
ccty[i]=&#x27;*&#x27;;
}
return ret;
}
int main() {
    cin &gt;&gt; s;
    char cha = s[0];
    while (cha != &#x27;@&#x27;) {
        if (flag == 0) i = 0;
        if (cha &gt;= &#x27;0&#x27; &amp;&amp; cha &lt;= &#x27;9&#x27;) {
            flag = 1;
            ccty[i] = cha;
            i++;
        }
        if (cha == &#x27;.&#x27;) {
            flag = 0;
            Num = makernum();
            num.push(Num);
        }
        if ((cha &lt; &#x27;0&#x27; || cha &gt; &#x27;9&#x27;) &amp;&amp; cha != &#x27;.&#x27;) {
            numa = num.top(); num.pop();
            numb = num.top(); num.pop();
            if (cha == &#x27;+&#x27;) num.push(numb + numa);
            if (cha == &#x27;-&#x27;) num.push(numb - numa);
            if (cha == &#x27;*&#x27;) num.push(numb * numa);
            if (cha == &#x27;/&#x27;) num.push(numb / numa);
        }
        k++;
        cha = s[k];
    }
    cout &lt;&lt; num.top() &lt;&lt; endl;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1449%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1449%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 17 Dec 2024 08:48:42 GMT</pubDate></item><item><title><![CDATA[洛谷 P5788 【模板】单调栈]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5788%E5%8D%95%E8%B0%83%E6%A0%88">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5788%E5%8D%95%E8%B0%83%E6%A0%88</a></blockquote><div><h1 id="p5788">【模板】单调栈（P5788）</h1><h2 id="">题目</h2><p>题目描述：</p><p>给出项数为 $n$ 的整数数列 $a_{1 \dots n}$。</p><p>定义函数 $f(i)$ 代表数列中第 $i$ 个元素之后第一个大于 $a<em>i$ 的元素的<strong>下标</strong>，即 $f(i)=\min</em>{i&lt;j\leq n, a<em>j &gt; a</em>i} {j}$。若不存在，则 $f(i)=0$。</p><p>试求出 $f(1\dots n)$。</p><p>输入格式：</p><p>第一行一个正整数 $n$。</p><p>第二行 $n$ 个正整数 $a_{1\dots n}$。</p><p>输出格式：</p><p>一行 $n$ 个整数表示 $f(1), f(2), \dots, f(n)$ 的值。</p><p>数据范围与说明：</p><p>【数据规模与约定】</p><p>对于 $30%$ 的数据，$n\leq 100$；</p><p>对于 $60%$ 的数据，$n\leq 5 \times 10^3$ ；</p><p>对于 $100%$ 的数据，$1 \le n\leq 3\times 10^6$，$1\leq a_i\leq 10^9$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5
1 4 2 3 5
</code></pre>
<p>输出：</p><pre class=""><code class="">2 5 4 5 0
</code></pre>
<h2 id="">题意</h2><p>给定一个长度为 $n$ 的序列 $a$，定义 $f(i)$ 为第 $i$ 个元素之后第一个严格大于 $a_i$ 的元素下标，不存在则为 $0$。输出 $f(1\ldots n)$。</p><h2 id="">思路</h2><p>从右向左维护“严格递减”的单调栈：</p><ul><li>栈中存储下标，且对应的值严格递减；</li><li>对于当前位置 $i$，不断弹出小于等于 $a_i$ 的下标，剩余的栈顶即为第一个更大的位置；</li><li>若栈为空，则 $f(i)=0$；</li><li>处理完后将 $i$ 入栈继续向左。</li></ul><p>时间复杂度 $O(n)$。</p><h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;

const int N = 3&#x27;000&#x27;000 + 5;
int a[N], ans[N];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n; 
    cin &gt;&gt; n;
    for (int i = 1; i &lt;= n; ++i) cin &gt;&gt; a[i];

    stack&lt;int&gt; st;
    for (int i = n; i &gt;= 1; --i) {
        while (!st.empty() &amp;&amp; a[st.top()] &lt;= a[i]) st.pop();
        ans[i] = st.empty() ? 0 : st.top();
        st.push(i);
    }

    for (int i = 1; i &lt;= n; ++i) {
        cout &lt;&lt; ans[i] &lt;&lt; (i == n ? &#x27;\n&#x27; : &#x27; &#x27;);
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5788%E5%8D%95%E8%B0%83%E6%A0%88</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5788%E5%8D%95%E8%B0%83%E6%A0%88</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Thu, 12 Dec 2024 15:03:53 GMT</pubDate></item><item><title><![CDATA[【题解】P5788]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5788%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5788%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p5788">P5788</h1><h2 id="">题目</h2><p>题目描述：</p><p>给出项数为 $n$ 的整数数列 $a_{1 \dots n}$。</p><p>定义函数 $f(i)$ 代表数列中第 $i$ 个元素之后第一个大于 $a<em>i$ 的元素的<strong>下标</strong>，即 $f(i)=\min</em>{i&lt;j\leq n, a<em>j &gt; a</em>i} {j}$。若不存在，则 $f(i)=0$。</p><p>试求出 $f(1\dots n)$。</p><p>输入格式：</p><p>第一行一个正整数 $n$。</p><p>第二行 $n$ 个正整数 $a_{1\dots n}$。</p><p>输出格式：</p><p>一行 $n$ 个整数表示 $f(1), f(2), \dots, f(n)$ 的值。</p><p>数据范围与说明：</p><p>【数据规模与约定】</p><p>对于 $30%$ 的数据，$n\leq 100$；</p><p>对于 $60%$ 的数据，$n\leq 5 \times 10^3$ ；</p><p>对于 $100%$ 的数据，$1 \le n\leq 3\times 10^6$，$1\leq a_i\leq 10^9$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5
1 4 2 3 5
</code></pre>
<p>输出：</p><pre class=""><code class="">2 5 4 5 0
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int s[3000005],ans[3000005];
int main(){
    int n;
    scanf(&quot;%d&quot;,&amp;n);
    for(int i=1;i&lt;=n;i++){
        scanf(&quot;%d&quot;,&amp;s[i]);
    }
    stack&lt;int&gt; st;
    for(int i=n;i&gt;=1;i--){
        while(!st.empty()&amp;&amp;s[st.top()]&lt;=s[i]){      
            st.pop();
        }
        if(st.empty()){
            ans[i]=0;
        }else{
            ans[i]=st.top();
        }
        st.push(i);
    }
    for(int i=1;i&lt;=n;i++){
        printf(&quot;%d &quot;,ans[i]);
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5788%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P5788%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Thu, 12 Dec 2024 15:03:53 GMT</pubDate></item><item><title><![CDATA[【题解】P2947]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2947%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2947%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p2947">P2947</h1><h2 id="">题目</h2><p>题目描述：</p><p>约翰的 $N(1\le N\le10^5)$ 头奶牛站成一排，奶牛 $i$ 的身高是 $H<em>i(1\le H</em>i\le10^6)$。现在，每只奶牛都在向右看。对于奶牛 $i$，如果奶牛 $j$ 满足 $i&lt;j$ 且 $H<em>i&lt;H</em>j$，我们可以说奶牛 $i$ 可以仰望奶牛 $j$。 求出每只奶牛离她最近的仰望对象。</p><p>输入格式：</p><p>第 $1$ 行输入 $N$，之后 $N$ 行第 $i+1$ 行输入一个身高 $H_i$。</p><p>输出格式：</p><p>共 $N$ 行，按顺序每行输出一只奶牛的最近仰望对象，如果没有仰望对象，输出 $0$。</p><p>数据范围与说明：</p><p><strong>【输入说明】</strong></p><p>$6$ 头奶牛的身高分别为 $3$, $2$, $6$, $1$, $1$, $2$。</p><p><strong>【输出说明】</strong></p><p>奶牛 $1,2$ 仰望奶牛 $3$，奶牛 $4,5$ 仰望奶牛 $6$，奶牛 $3$ 和 $6$ 没有仰望对象。</p><p><strong>【数据规模】</strong></p><p>对于 $20%$ 的数据：$1\le N\le10$；</p><p>对于 $50%$ 的数据：$1\le N\le10^3$；</p><p>对于 $100%$ 的数据：$1\le N\le10^5,1\le H_i\le10^6$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">6 
3 
2 
6 
1 
1 
2
</code></pre>
<p>输出：</p><pre class=""><code class="">3 
3 
0 
6 
6 
0
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int h[100001],ans[100001];
int main(){
    int n;
    scanf(&quot;%d&quot;,&amp;n);
    for(int i=1;i&lt;=n;i++){
        scanf(&quot;%d&quot;,&amp;h[i]);
    }
    stack&lt;int&gt; st;
    for(int i=n;i&gt;=1;i--){
        while(!st.empty()&amp;&amp;h[st.top()]&lt;=h[i]){
            st.pop();
        }
        if(st.empty()){
            ans[i]=0;
        }else{
            ans[i]=st.top();
        }
        st.push(i);
    }
    for(int i=1;i&lt;=n;i++){
        printf(&quot;%d\n&quot;,ans[i]);
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2947%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2947%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Thu, 12 Dec 2024 12:10:42 GMT</pubDate></item><item><title><![CDATA[【题解】P1540 [NOIP 2010 提高组] 机器翻译]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1540%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1540%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1540-noip-2010--httpswwwluogucomcnproblemp1540">P1540 [NOIP 2010 提高组] 机器翻译（<a href="https://www.luogu.com.cn/problem/P1540">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>小晨的电脑上安装了一个机器翻译软件，他经常用这个软件来翻译英语文章。</p><p>这个翻译软件的原理很简单，它只是从头到尾，依次将每个英文单词用对应的中文含义来替换。对于每个英文单词，软件会先在内存中查找这个单词的中文含义，如果内存中有，软件就会用它进行翻译；如果内存中没有，软件就会在外存中的词典内查找，查出单词的中文含义然后翻译，并将这个单词和译义放入内存，以备后续的查找和翻译。</p><p>假设内存中有 $M$ 个单元，每单元能存放一个单词和译义。每当软件将一个新单词存入内存前，如果当前内存中已存入的单词数不超过 $M-1$，软件会将新单词存入一个未使用的内存单元；若内存中已存入 $M$ 个单词，软件会清空最早进入内存的那个单词，腾出单元来，存放新单词。</p><p>假设一篇英语文章的长度为 $N$ 个单词。给定这篇待译文章，翻译软件需要去外存查找多少次词典？假设在翻译开始前，内存中没有任何单词。</p><p>输入格式：</p><p>共 $2$ 行。每行中两个数之间用一个空格隔开。</p><p>第一行为两个正整数 $M,N$，代表内存容量和文章的长度。</p><p>第二行为 $N$ 个非负整数，按照文章的顺序，每个数（大小不超过 $1000$）代表一个英文单词。文章中两个单词是同一个单词，当且仅当它们对应的非负整数相同。</p><p>输出格式：</p><p>一个整数，为软件需要查词典的次数。</p><p>数据范围与说明：</p><h3 id="">样例解释</h3><p>整个查字典过程如下：每行表示一个单词的翻译，冒号前为本次翻译后的内存状况：</p><ol start="1"><li><code>1</code>：查找单词 1 并调入内存。</li><li><code>1 2</code>：查找单词 2 并调入内存。</li><li><code>1 2</code>：在内存中找到单词 1。</li><li><code>1 2 5</code>：查找单词 5 并调入内存。</li><li><code>2 5 4</code>：查找单词 4 并调入内存替代单词 1。</li><li><code>2 5 4</code>：在内存中找到单词 4。</li><li><code>5 4 1</code>：查找单词 1 并调入内存替代单词 2。</li></ol><p>共计查了 $5$ 次词典。</p><h3 id="">数据范围</h3><ul><li>对于 $10%$ 的数据有 $M=1$，$N \leq 5$；</li><li>对于 $100%$ 的数据有 $1 \leq M \leq 100$，$1 \leq N \leq 1000$。</li></ul><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">3 7
1 2 1 5 4 4 1
</code></pre>
<p>输出：</p><pre class=""><code class="">5
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int n,m,l,r,a[1005],b[1005],x,ans;
int main(){
    cin&gt;&gt;m&gt;&gt;n;
    l=0;r=0;
    for(int i=1;i&lt;=n;i++){
        scanf(&quot;%d&quot;,&amp;x);
        if(a[x]==0){
            ans++;
            r++;
            b[r]=x;
            a[x]=1;
            if(r&gt;m){
            l++;
            a[b[l]]=0;
        }
        }
    }
    cout&lt;&lt;ans;
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1540%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1540%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Mon, 09 Dec 2024 15:43:55 GMT</pubDate></item><item><title><![CDATA[【题解】P2422]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2422%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2422%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p2422">P2422</h1><h2 id="">题目</h2><p>题目描述：</p><p>kkk 做了一个人体感觉分析器。每一天，人都有一个感受值 $A<em>i$，$A</em>i$ 越大，表示人感觉越舒适。在一段时间 $\left[i, j\right]$ 内，人的舒适程度定义为 $\left[i, j\right]$ 中最不舒服的那一天的感受值 $\times$ $\left[i, j\right]$中每一天感受值的和。现在给出 kkk 在连续 $N$ 天中的感受值，请问，在哪一段时间，kkk 感觉最舒适？</p><p>输入格式：</p><p>第一行为 $N$，代表数据记录的天数。</p><p>第二行 $N$ 个整数，代表每一天的感受值。</p><p>输出格式：</p><p>一行，表示在最舒适的一段时间中的感受值。</p><p>数据范围与说明：</p><p>kkk 最开心的一段时间是第 $3$ 天到第 $5$ 天，开心值：$(6+4+5)\times4=60$。</p><p>对于 $30%$ 的数据，$1\le N\le 100$。</p><p>对于 $70%$ 的数据，$1\le N\le 2000$。</p><p>对于 $100%$ 的数据，$1\le N\le 10^5$，$1\le \texttt{感受值}\le 10^6$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">6
3 1 6 4 5 2
</code></pre>
<p>输出：</p><pre class=""><code class="">60
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
long long a[100006],s[100006],f[100006],q[100006],ans=0,tail;
int main(){
    int n;
    scanf(&quot;%d&quot;,&amp;n);
    for(int i=1;i&lt;=n;i++){
        scanf(&quot;%lld&quot;,&amp;a[i]);
    }
    n++;
    a[n]=0;
    for(register int i=1;i&lt;=n;i++){
        s[i]=s[i-1]+a[i];
    }
    for(register int i=1;i&lt;=n;i++){
        while(a[q[tail]]&gt;a[i]){
            f[q[tail]]+=(s[i-1]-s[q[tail]]);
            tail--;
        }
        f[i]=s[i]-s[q[tail]];
        q[++tail]=i;
    }
    for(register int i=1;i&lt;=n-1;i++){
        ans=max(ans,f[i]*a[i]);
    }
    printf(&quot;%lld&quot;,ans);
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2422%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2422%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 07 Dec 2024 12:33:10 GMT</pubDate></item><item><title><![CDATA[【题解】P2629]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2629%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2629%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p2629">P2629</h1><h2 id="">题目</h2><p>题目描述：</p><p>Uim 在公司里面当秘书，现在有 $n$ 条消息要告知老板。每条消息有一个好坏度，这会影响老板的心情。告知完一条消息后，老板的心情等于老板之前的心情加上这条消息的好坏度。最开始老板的心情是 $0$，一旦老板心情到了 $0$ 以下就会勃然大怒，炒了 Uim 的鱿鱼。</p><p>Uim 为了不被炒，提前知道了这些消息（已经按时间的发生顺序进行了排列）的好坏度，希望知道如何才能不让老板发怒。</p><p>Uim 可以使用一种叫 “倒叙” 的手法，例如有 $n$ 条消息，Uim 可以任取一个整数 $k$（$1 \leq k \leq n$），先从 $k$ 事件通报到 $n$ 事件，再从 $1$ 事件通报到 $k-1$ 事件。特别的，当 $k=1$ 时按照原顺序通报。</p><p>他希望知道，有多少个这样的 $k$ 可以让老板不发怒。</p><p>输入格式：</p><p>第一行一个整数 $n$（$1 \le n \le10^6$），表示有 $n$ 个消息。</p><p>第二行 $n$ 个整数，按时间顺序给出第 $i$ 条消息的好坏度 $A<em>i$（$-10^3\le A</em>i \le 10^3$）。</p><p>输出格式：</p><p>一行一个整数，表示可行的方案个数。</p><p>数据范围与说明：</p><p><strong>【样例解释】</strong></p><p>通报事件的可行顺序（用编号表示）为 $2\rightarrow3\rightarrow4\rightarrow1$ 或 $3\rightarrow4\rightarrow1\rightarrow2$（分别对应 $k=2$ 和 $k=3$）</p><p>通报事件的可行顺序（用好坏度表示）为 $5\rightarrow1\rightarrow2\rightarrow(-3)$ 或 $1\rightarrow2\rightarrow(-3)\rightarrow5$</p><p><strong>【数据范围】</strong></p><p>对于 $25%$ 的数据，$n\le10^3$；<br/>对于 $75%$ 的数据，$n\le10^4$；<br/>对于 $100%$ 的数据，$1 \le n\le 10^6$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">4
-3 5 1 2
</code></pre>
<p>输出：</p><pre class=""><code class="">2
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
long long q[20000006],a[2000006],s[2000006];
int main(){
    int n,l=1,r=0,ans=0;
    scanf(&quot;%d&quot;,&amp;n);
    for(register int i=1;i&lt;=n;i++){
        scanf(&quot;%lld&quot;,&amp;a[i]);
    }
    for(register int i=1;i&lt;=n-1;i++){
        a[i+n]=a[i];
    }
    for(register int i=1;i&lt;=2*n-1;i++){
        s[i]=s[i-1]+a[i];
    }
    for(register int i=1;i&lt;=2*n-1;i++){
        while(l&lt;=r&amp;&amp;max(i-n+1,1)&gt;q[l]){
            l++;
        }
        while(l&lt;=r&amp;&amp;s[q[r]]&gt;=s[i]){
            r--;
        }
        q[++r]=i;
        if(i-n+1&gt;0&amp;&amp;s[q[l]]-s[i-n]&gt;=0){
            ans++;
        }
    }
    printf(&quot;%d\n&quot;,ans);
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2629%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2629%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 06 Dec 2024 11:22:16 GMT</pubDate></item><item><title><![CDATA[【题解】P1714]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1714%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1714%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1714">P1714</h1><h2 id="">题目</h2><p>题目描述：</p><p>今天是小 Z 的生日，同学们为他带来了一块蛋糕。这块蛋糕是一个长方体，被用不同色彩分成了 $n$ 个相同的小块，每小块都有对应的幸运值。</p><p>小 Z 作为寿星，自然希望吃到的蛋糕的幸运值总和最大，但小 Z 最多又只能吃 $m(m\le n)$ 小块的蛋糕。</p><p>请你帮他从这 $n$ 小块中找出<strong>连续</strong>的 $k(1 \le k\le m)$ 块蛋糕，使得其上的总幸运值最大。</p><p><strong>形式化地</strong>，在数列 ${p<em>n}$ 中，找出一个子段 $[l,r](r-l+1\le m)$，最大化 $\sum\limits</em>{i=l}^rp_i$。</p><p>输入格式：</p><p>第一行两个整数 $n,m$。分别代表共有 $n$ 小块蛋糕，小 Z 最多只能吃 $m$ 小块。</p><p>第二行 $n$ 个整数，第 $i$ 个整数 $p_i$ 代表第 $i$ 小块蛋糕的幸运值。</p><p>输出格式：</p><p>仅一行一个整数，即小 Z 能够得到的最大幸运值。</p><p>数据范围与说明：</p><h4 id="">数据规模与约定</h4><ul><li>对于 $20%$ 的数据，有 $1\le n\le100$。</li><li>对于 $100%$ 的数据，有 $1\le n\le5\times 10^5$，$|p_i|≤500$。</li></ul><p>保证答案的绝对值在 $[0,2^{31}-1]$ 之内。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5 2
1 2 3 4 5
</code></pre>
<p>输出：</p><pre class=""><code class="">9
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">6 3
1 -2 3 -4 5 -6
</code></pre>
<p>输出：</p><pre class=""><code class="">5
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
int ans=-233333333,n,m,a,sum[500006];
deque&lt;int&gt;q;
int main(){
    cin&gt;&gt;n&gt;&gt;m;
    for(int i=1;i&lt;=n;i++){
        cin&gt;&gt;a;
        sum[i]=sum[i-1]+a;
    }
    q.push_back(0);
    for(int i=1;i&lt;=n;i++){
        while(q.front()+m&lt;i){
            q.pop_front();
        }
        ans=max(ans,sum[i]-sum[q.front()]);
        while(!q.empty()&amp;&amp;sum[q.back()]&gt;=sum[i])
        {
            q.pop_back();
        }
        q.push_back(i);
    }
     cout&lt;&lt;ans;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1714%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1714%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Thu, 05 Dec 2024 13:25:19 GMT</pubDate></item><item><title><![CDATA[【题解】P2032]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2032%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2032%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p2032">P2032</h1><h2 id="">题目</h2><p>题目描述：</p><p>有一个 $1 \times n$ 的矩阵，有 $n$ 个整数。</p><p>现在给你一个可以盖住连续 $k$ 个数的木板。</p><p>一开始木板盖住了矩阵的第 $1 \sim k$ 个数，每次将木板向右移动一个单位，直到右端与第 $n$ 个数重合。</p><p>每次移动前输出被覆盖住的数字中最大的数是多少。</p><p>输入格式：</p><p>第一行两个整数 $n,k$，表示共有 $n$ 个数，木板可以盖住 $k$ 个数。</p><p>第二行 $n$ 个整数，表示矩阵中的元素。</p><p>输出格式：</p><p>共 $n - k + 1$ 行，每行一个整数。</p><p>第 $i$ 行表示第 $i \sim i + k - 1$ 个数中最大值是多少。</p><p>数据范围与说明：</p><p>对于 $20%$ 的数据，$1 \leq k \leq n \leq 10^3$。</p><p>对于 $50%$ 的数据，$1 \leq k \leq n \leq 10^4$。</p><p>对于 $100%$ 的数据，$1 \leq k \leq n \leq 2 \times 10^6$，矩阵中的元素大小不超过 $10^4$ 并且均为正整数。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5 3
1 5 3 4 2
</code></pre>
<p>输出：</p><pre class=""><code class="">5
5
4
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
int q[2000006];
int a[2000006];
int main(){
    int n,k;
    cin&gt;&gt;n&gt;&gt;k;
    int left=1;
    int right=1;
    for(int i=1;i&lt;=n;i++){
        scanf(&quot;%d&quot;,&amp;a[i]);
    }
    q[1]=1;
    for(int i=1;i&lt;=n;i++){
        if(q[left]&lt;=i-k){
            left++;
        }
        while(left&lt;=right&amp;&amp;a[q[right]]&lt;=a[i]){
            right--;
        }
        q[++right]=i;
        if(i&gt;=k){
            printf(&quot;%d\n&quot;,a[q[left]]);
        }
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2032%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P2032%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 04 Dec 2024 00:50:42 GMT</pubDate></item><item><title><![CDATA[【题解】P1440]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1440%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1440%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1440">P1440</h1><h2 id="">题目</h2><p>题目描述：</p><p>一个含有 $n$ 项的数列，求出每一项前的 $m$ 个数到它这个区间内的最小值。若前面的数不足 $m$ 项则从第 $1$ 个数开始，若前面没有数则输出 $0$。</p><p>输入格式：</p><p>第一行两个整数，分别表示 $n$，$m$。</p><p>第二行，$n$ 个正整数，为所给定的数列 $a_i$。</p><p>输出格式：</p><p>$n$ 行，每行一个整数，第 $i$ 个数为序列中 $a_i$ 之前 $m$ 个数的最小值。</p><p>数据范围与说明：</p><p>对于 $100%$ 的数据，保证 $1\le m\le n\le2\times10^6$，$1\le a_i\le3\times10^7$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">6 2
7 8 1 4 3 2
</code></pre>
<p>输出：</p><pre class=""><code class="">0
7
7
1
1
3
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int q[2000005], a[2000005];
int main(){
    int n,m;
    scanf(&quot;%d %d&quot;,&amp;n,&amp;m);
    for(int i=1;i&lt;=n;i++)    scanf(&quot;%d&quot;,&amp;a[i]);
    int front=1,rear=0;
    printf(&quot;0\n&quot;);
    for(int i=1;i&lt;=n-1;i++){
        while(front&lt;=rear&amp;&amp;q[front]&lt;=i-m){
            front++;
        }
        while(front&lt;=rear&amp;&amp;a[q[rear]]&gt;=a[i]){
            rear--;
        }
        q[++rear]=i;
        printf(&quot;%d\n&quot;,a[q[front]]);
    }
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1440%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1440%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 03 Dec 2024 17:10:54 GMT</pubDate></item><item><title><![CDATA[【题解】P8637]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P8637%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P8637%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p8637httpswwwluogucomcnproblemp8637">P8637（<a href="https://www.luogu.com.cn/problem/P8637">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>有 $N$ 个瓶子，编号 $1 \sim N$，放在架子上。</p><p>比如有 $5$ 个瓶子：</p><p>$$2,1,3,5,4$$</p><p>要求每次拿起 $2$ 个瓶子，交换它们的位置。</p><p>经过若干次后，使得瓶子的序号为：</p><p>$$1,2,3,4,5$$</p><p>对于这么简单的情况，显然，至少需要交换 $2$ 次就可以复位。</p><p>如果瓶子更多呢？你可以通过编程来解决。</p><p>输入格式：</p><p>第一行：一个正整数 $N$（$N&lt;10000$），表示瓶子的数目。</p><p>第二行：$N$ 个正整数，用空格分开，表示瓶子目前的排列情况。</p><p>输出格式：</p><p>输出数据为一行一个正整数，表示至少交换多少次，才能完成排序。</p><p>数据范围与说明：</p><p>时限 1 秒, 256M。蓝桥杯 2016 年第七届省赛</p><p>蓝桥杯 2016 年省赛 B 组 I 题。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5
3 1 2 5 4
</code></pre>
<p>输出：</p><pre class=""><code class="">3
</code></pre>
<p>输入输出样例 #2</p><p>输入：</p><pre class=""><code class="">5
5 4 3 2 1
</code></pre>
<p>输出：</p><pre class=""><code class="">2
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
int main(){
    int arr[10001];
    int n,count=0;
    scanf(&quot;%d&quot;,&amp;n);
    for(int i=1;i&lt;=n;i++){
        scanf(&quot;%d&quot;,&amp;arr[i]);
    } 
    for(int i=1;i&lt;=n;i++){
        if(arr[i]!=i){
            for(int j=1;j&lt;=n;j++){
                if(arr[j]==i){
                    swap(arr[j],arr[i]);
                    count++;
                }
            }
        }
    }
    printf(&quot;%d &quot;,count);
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P8637%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P8637%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Fri, 29 Nov 2024 07:20:11 GMT</pubDate></item><item><title><![CDATA[【题解】P10904]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10904%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10904%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p10904httpswwwluogucomcnproblemp10904">P10904（<a href="https://www.luogu.com.cn/problem/P10904">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>小蓝正在数轴上挖矿，数轴上一共有 $n$ 个矿洞，第 $i$ 个矿洞的坐标为 $a_i$。小蓝从 $0$ 出发，每次可以向左或向右移动 $1$ 的距离，当路过一个矿洞时，就会进行挖矿作业，获得 $1$ 单位矿石，但一个矿洞不能被多次挖掘。小蓝想知道在<br/>移动距离不超过 $m$ 的前提下，最多能获得多少单位矿石？</p><p>输入格式：</p><p>输入的第一行包含两个正整数 $n,m$，用一个空格分隔。</p><p>第二行包含 $n$ 个整数 $a<em>1, a</em>2,\cdots, a_n$，相邻整数之间使用一个空格分隔。</p><p>输出格式：</p><p>输出一行包含一个整数表示答案。</p><p>数据范围与说明：</p><p><strong>【样例说明】</strong></p><p>路径：$0\to -1\to 0\to 1\to 2$，可以对 ${0,-1,1,2}$ 四个矿洞挖掘并获得最多 $4$ 块矿石。</p><p><strong>【评测用例规模与约定】</strong></p><p>对于 $20%$ 的评测用例，$1 \le n \le 10^3$；<br/>对于所有评测用例，$1 \le n \le 10^5$，$-10^6 \le a_i \le 10^6$，$1 \le m \le 2 \times 10^6$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5 4
0 -3 -1 1 2
</code></pre>
<p>输出：</p><pre class=""><code class="">4
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>小蓝正在数轴上挖矿，数轴上一共有 $n$ 个矿洞，第 $i$ 个矿洞的坐标为 $a_i$。小蓝从 $0$ 出发，每次可以向左或向右移动 $1$ 的距离，当路过一个矿洞时，就会进行挖矿作业，获得 $1$ 单位矿石，但一个矿洞不能被多次挖掘。小蓝想知道在<br/>移动距离不超过 $m$ 的前提下，最多能获得多少单位矿石？</p><h2 id="">代码</h2><h3 id="c">C</h3><pre class="language-c lang-c"><code class="language-c lang-c">#include &lt;stdio.h&gt;

const int N = 2e6 + 10;
int n,m,l[1000],r[1000],ans,cnt;

int max(int a, int b) {
    return (a &gt; b) ? a : b;
}
int main(){
    
    scanf(&quot;%d %d&quot;,&amp;n,&amp;m);
    for(int i=1,x;i&lt;=n;i++){
        scanf(&quot;%d&quot;,&amp;x);
        if(x&lt;0){
            l[-x]++;
        }else if(x&gt;0){
            r[x]++;
        }else{
            cnt++;
        }
    }
    for(int i=1;i&lt;=m;i++){
        l[i]+=l[i-1];
        r[i]+=r[i-1];
    }
    for(int i=1,t;i&lt;=m;i++){
        t=l[i];
        if(m-i*2&gt;0){
            t+=r[m-i*2];
        }
        ans=max(ans,t);
        t=r[i];
        if(m-i*2&gt;0){
            t+=l[m-i*2];
        }
        ans=max(ans,t);
    }
    printf(&quot;%d\n&quot;,ans+cnt);
    return 0;
}
</code></pre>
<h3 id="c">C++</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;iostream&gt;

using namespace std;

const int N = 2e6 + 10;
int n,m,l[N],r[N],ans,cnt;

int main(){
    scanf(&quot;%d %d&quot;,&amp;n,&amp;m);
    for(int i=1,x;i&lt;=n;i++){
        scanf(&quot;%d&quot;,&amp;x);
        if(x&lt;0){
            l[-x]++;
        }else if(x&gt;0){
            r[x]++;
        }else{
            cnt++;
        }
    }
    for(int i=1;i&lt;=m;i++){
        l[i]+=l[i-1];
        r[i]+=r[i-1];
    }
    for(int i=1,t;i&lt;=m;i++){
        t=l[i];
        if(m-i*2&gt;0){
            t+=r[m-i*2];
        }
        
        
        ans=max(ans,t);
        t=r[i];
        if(m-i*2&gt;0){
            t+=l[m-i*2];
        }
        ans=max(ans,t);
    }
    printf(&quot;%d\n&quot;,ans+cnt);
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10904%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P10904%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 26 Nov 2024 17:31:13 GMT</pubDate></item><item><title><![CDATA[【题解】P1305 新二叉树]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1305%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1305%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1305-httpswwwluogucomcnproblemp1305">P1305 新二叉树（<a href="https://www.luogu.com.cn/problem/P1305">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>输入一串二叉树，输出其前序遍历。</p><p>输入格式：</p><p>第一行为二叉树的节点数 $n$。($1 \leq n \leq 26$)</p><p>后面 $n$ 行，第一个字母为节点，后两个字母分别为其左右儿子。特别地，数据保证第一行读入的节点必为根节点。</p><p>空节点用 <code>*</code> 表示</p><p>输出格式：</p><p>二叉树的前序遍历。</p><p>数据范围与说明：</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">6
abc
bdi
cj*
d**
i**
j**
</code></pre>
<p>输出：</p><pre class=""><code class="">abdicj
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
struct programmer{
    char lc;
    char rc;
}lt[130];
char h,h1;
void d(char x){
    if(x==&#x27;*&#x27;){
        return;
    }
    cout&lt;&lt;x;
    d(lt[x].lc);
    d(lt[x].rc);
}
int main(){
    int n;
    scanf(&quot;%d&quot;,&amp;n);
    cin&gt;&gt;h1;
    cin&gt;&gt;lt[h1].lc;
    cin&gt;&gt;lt[h1].rc;
    for(int i=2;i&lt;=n;i++){
        cin&gt;&gt;h;
        cin&gt;&gt;lt[h].lc;
        cin&gt;&gt;lt[h].rc;
    }
    d(h1);
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1305%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1305%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Tue, 26 Nov 2024 15:47:59 GMT</pubDate></item><item><title><![CDATA[【题解】P1886 【模板】单调队列 / 滑动窗口]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1886%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1886%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1886---httpswwwluogucomcnproblemp1886">P1886 【模板】单调队列 / 滑动窗口（<a href="https://www.luogu.com.cn/problem/P1886">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>有一个长为 $n$ 的序列 $a$，以及一个大小为 $k$ 的窗口。现在这个窗口从左边开始向右滑动，每次滑动一个单位，求出每次滑动后窗口中的最小值和最大值。</p><p>例如，对于序列 $[1,3,-1,-3,5,3,6,7]$ 以及 $k = 3$，有如下过程：</p><p>$$\def\arraystretch{1.2}\begin{array}{|c|c|c|}\hline\textsf{窗口位置} &amp; \textsf{最小值} &amp; \textsf{最大值} \ \hline\verb![1   3  -1] -3   5   3   6   7 ! &amp; -1 &amp; 3 \ \hline\verb! 1  [3  -1  -3]  5   3   6   7 ! &amp; -3 &amp; 3 \ \hline\verb! 1   3 [-1  -3   5]  3   6   7 ! &amp; -3 &amp; 5 \ \hline\verb! 1   3  -1 [-3   5   3]  6   7 ! &amp; -3 &amp; 5 \ \hline\verb! 1   3  -1  -3  [5   3   6]  7 ! &amp; 3 &amp; 6 \ \hline\verb! 1   3  -1  -3   5  [3   6   7]! &amp; 3 &amp; 7 \ \hline\end{array}$$</p><p>输入格式：</p><p>输入一共有两行，第一行有两个正整数 $n,k$；<br/>第二行有 $n$ 个整数，表示序列 $a$。</p><p>输出格式：</p><p>输出共两行，第一行为每次窗口滑动的最小值；<br/>第二行为每次窗口滑动的最大值。</p><p>数据范围与说明：</p><p>【数据范围】<br/>对于 $50%$ 的数据，$1 \le n \le 10^5$；<br/>对于 $100%$ 的数据，$1\le k \le n \le 10^6$，$a_i \in [-2^{31},2^{31})$。</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">8 3
1 3 -1 -3 5 3 6 7
</code></pre>
<p>输出：</p><pre class=""><code class="">-1 -3 -3 -3 3 3
3 3 5 5 6 7
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include&lt;bits/stdc++.h&gt;
using namespace std;
int n,k;
deque&lt;int&gt;q;
int a[1000005];
int main(){
    cin&gt;&gt;n&gt;&gt;k;
    for(int i=0;i&lt;n;i++){
        cin&gt;&gt;a[i];
    }
    for(int i=0;i&lt;n;i++){
        if(!q.empty()&amp;&amp;q.front()&lt;=i-k){
            q.pop_front();
        }
        while(!q.empty()&amp;&amp;a[q.back()]&gt;=a[i]){
            q.pop_back();
        }
        q.push_back(i);
        if(i&gt;=k-1){
            cout&lt;&lt;a[q.front()]&lt;&lt;&quot; &quot;;
        }
    }
    cout&lt;&lt;endl;
    q.clear();
    for(int i=0;i&lt;n;i++){
        if(!q.empty()&amp;&amp;q.front()&lt;=i-k){
            q.pop_front();
        }
        while(!q.empty()&amp;&amp;a[q.back()]&lt;=a[i]){
            q.pop_back();
        }
        q.push_back(i);
        if(i&gt;=k-1){
            cout&lt;&lt;a[q.front()]&lt;&lt;&quot; &quot;;
        }
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1886%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1886%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Sat, 09 Nov 2024 13:46:07 GMT</pubDate></item><item><title><![CDATA[【题解】P1085]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1085%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1085%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="p1085httpswwwluogucomcnproblemp1085">P1085（<a href="https://www.luogu.com.cn/problem/P1085">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>津津上初中了。妈妈认为津津应该更加用功学习，所以津津除了上学之外，还要参加妈妈为她报名的各科复习班。另外每周妈妈还会送她去学习朗诵、舞蹈和钢琴。但是津津如果一天上课超过八个小时就会不高兴，而且上得越久就会越不高兴。假设津津不会因为其它事不高兴，并且她的不高兴不会持续到第二天。请你帮忙检查一下津津下周的日程安排，看看下周她会不会不高兴；如果会的话，哪天最不高兴。</p><p>输入格式：</p><p>输入包括 $7$ 行数据，分别表示周一到周日的日程安排。每行包括两个小于 $10$ 的非负整数，用空格隔开，分别表示津津在学校上课的时间和妈妈安排她上课的时间。</p><p>输出格式：</p><p>一个数字。如果不会不高兴则输出 $0$，如果会则输出最不高兴的是周几（用 $1, 2, 3, 4, 5, 6, 7$ 分别表示周一，周二，周三，周四，周五，周六，周日）。如果有两天或两天以上不高兴的程度相当，则输出时间最靠前的一天。</p><p>数据范围与说明：</p><p>NOIP2004 普及组第 1 题</p><ul><li>2021-10-27：增加一组 hack 数据</li><li>2022-06-05：又增加一组 hack 数据</li></ul><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">5 3
6 2
7 2
5 3
5 4
0 4
0 6
</code></pre>
<p>输出：</p><pre class=""><code class="">3
</code></pre>
<h2 id="">题意</h2><p>简述：</p><p>津津上初中了。妈妈认为津津应该更加用功学习，所以津津除了上学之外，还要参加妈妈为她报名的各科复习班。另外每周妈妈还会送她去学习朗诵、舞蹈和钢琴。但是津津如果一天上课超过八个小时就会不高兴，而且上得越久就会越不高兴。假设津津不会因为其它事不高兴，并且她的不高兴不会持续到第二天。请你帮忙检查一下津津下周的日程安排，看看下周她会不会不高兴；如果会的话，哪天最不高兴。</p><h2 id="">代码</h2><h3 id="c">C</h3><pre class="language-c lang-c"><code class="language-c lang-c">#include &lt;stdio.h&gt;
int main()
{
    int x,y,sum,day=0,max=0;
    for(int i=1;i&lt;=7;i++){
        scanf(&quot;%d %d&quot;,&amp;x,&amp;y);
        sum=x+y;
        if(sum&gt;8&amp;&amp;sum&gt;max){
            max=sum;
            day=i;
        }
    }
    printf(&quot;%d&quot;,day); 
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1085%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1085%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Thu, 07 Nov 2024 15:42:18 GMT</pubDate></item><item><title><![CDATA[约瑟夫问题 - 题解]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1996%E9%A2%98%E8%A7%A3">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1996%E9%A2%98%E8%A7%A3</a></blockquote><div><h1 id="httpswwwluogucomcnproblemp1996">约瑟夫问题（<a href="https://www.luogu.com.cn/problem/P1996">洛谷题面</a>）</h1><h2 id="">题目</h2><p>题目描述：</p><p>$n$ 个人围成一圈，从第一个人开始报数,数到 $m$ 的人出列，再由下一个人重新从 $1$ 开始报数，数到 $m$ 的人再出圈，依次类推，直到所有的人都出圈，请输出依次出圈人的编号。</p><p><strong>注意：本题和《深入浅出-基础篇》上例题的表述稍有不同。书上表述是给出淘汰 $n-1$ 名小朋友，而该题是全部出圈。</strong></p><p>输入格式：</p><p>输入两个整数 $n,m$。</p><p>输出格式：</p><p>输出一行 $n$ 个整数，按顺序输出每个出圈人的编号。</p><p>数据范围与说明：</p><p>$1 \le m, n \le 100$</p><p>输入输出样例 #1</p><p>输入：</p><pre class=""><code class="">10 3
</code></pre>
<p>输出：</p><pre class=""><code class="">3 6 9 2 7 1 8 5 10 4
</code></pre>
<h2 id="">代码</h2><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;bits/stdc++.h&gt;
using namespace std;
int main(){
    int n,m;
    scanf(&quot;%d %d&quot;,&amp;n,&amp;m);
    queue&lt;int&gt; q;
    for(int i=1;i&lt;=n;i++){
        q.push(i);
    }
    int cnt=1;
    while(!q.empty()){
        if(cnt==m){
            cout&lt;&lt;q.front()&lt;&lt;&quot; &quot;;
            q.pop();
            cnt=1;
        }else{
            cnt++;
            q.push(q.front());
            q.pop();
        }
    }
    return 0;
}
</code></pre></div></div>]]></description><link>https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1996%E9%A2%98%E8%A7%A3</link><guid isPermaLink="true">https://binzi.top/posts/%E7%AE%97%E6%B3%95/P1996%E9%A2%98%E8%A7%A3</guid><dc:creator><![CDATA[biny]]></dc:creator><pubDate>Wed, 06 Nov 2024 13:58:31 GMT</pubDate></item></channel></rss>