<?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[Fisher's E-Garden]]></title><description><![CDATA[Hi 😊]]></description><link>https://www.amfishers.com</link><image><url>https://www.amfishers.com/i3he56xsmzbaina4e4.ico</url><title>Fisher&apos;s E-Garden</title><link>https://www.amfishers.com</link></image><generator>Shiro (https://github.com/Innei/Shiro)</generator><lastBuildDate>Fri, 30 Oct 2026 01:17:06 GMT</lastBuildDate><atom:link href="https://www.amfishers.com/feed" rel="self" type="application/rss+xml"/><pubDate>Fri, 30 Oct 2026 01:17:06 GMT</pubDate><language><![CDATA[en-US]]></language><item><title><![CDATA[AI Agent 开发 01｜先弄清楚，谁在执行任务]]></title><description><![CDATA[<div><blockquote>This render is generated by the Shiro API and may have formatting issues. For the best experience, visit:<a href="https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does">https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does</a></blockquote><div><p>让模型写一个待办应用，它可以给你一整页代码。代码出现在聊天框里，项目目录里却可能什么都没有。要跨过这一步，应用需要接收模型提出的操作，执行文件写入，再把结果送回去。</p><p>Agent 开发可以从这件小事开始理解。模型负责判断接下来做什么，程序负责让操作发生。一次操作带来的结果，会影响模型下一次的判断。</p><p>这套系列根据冴羽《AI Agent 开发实战》的学习整理稿重新编排，保留原教程来源，并补充工程上的判断。阅读时有 JavaScript 基础就够了。我们会逐步设计一个项目知识库助手，让它读取资料、回答问题、保留引用，最后接上网页。文中的演示数据和项目需求均用于学习。</p><h2 id="">把一次任务拆开看</h2><p>用户问项目应该怎样启动。助手首先需要知道当前项目用了什么工具，于是请求读取开发文档。程序检查文件是否允许读取，得到正文，交回模型。模型从正文中发现启动命令，再解释给用户。</p><p>这里有三个可以分别检查的结果。模型有没有选对文件，文件函数有没有读到内容，最后的回答有没有忠实使用内容。任何一处出错，都可能让用户拿到错误命令。</p><p>工具调用通常以结构化数据表达。下面只是我们自定义的教学格式。</p><pre class="language-json lang-json"><code class="language-json lang-json">{
  &quot;type&quot;: &quot;tool_call&quot;,
  &quot;name&quot;: &quot;read_project_doc&quot;,
  &quot;args&quot;: { &quot;path&quot;: &quot;development.md&quot; }
}</code></pre><p>模型输出这段数据以后，文件仍未被读取。宿主程序要查找工具、验证参数、执行函数，还得等待函数返回。文件内容只能来自这次读取。</p><p>把它继续做下去，就得到最基本的执行循环。</p><pre class="language-text lang-text"><code class="language-text lang-text">接收目标
→ 模型选择行动
→ 程序校验并执行工具
→ 工具结果进入消息历史
→ 模型继续行动或结束</code></pre><p>循环需要保存状态。它至少应该知道用户最初要求什么、已经读过什么、最近一次工具返回了什么。否则，下一轮模型可能又去读取同一个文件，也可能丢掉前面刚确认的限制。</p><h2 id="">这些名词分别放在哪里</h2><p>学教程时容易一次碰到 Memory、RAG、Workflow 和 MCP。可以先把它们放回刚才的任务。</p><table><thead><tr><th>能力</th><th>在知识库助手里解决的事情</th></tr></thead><tbody><tr><td>Tool</td><td>真正读取文档或查询资料</td></tr><tr><td>Agent Loop</td><td>根据当前结果决定是否继续查</td></tr><tr><td>Memory</td><td>在追问时带回相关前情</td></tr><tr><td>RAG</td><td>从一批资料里取出当前问题需要的内容</td></tr><tr><td>Workflow</td><td>固定必须经过的步骤和检查</td></tr><tr><td>MCP</td><td>让不同应用按共同协议连接外部能力</td></tr></tbody></table><p>它们可以组合，也可以单独使用。固定的文档问答可以由程序先检索，再调用模型生成答案。只有当下一步要随着反馈变化时，才需要增加模型选择行动的空间。</p><p>例如用户只想查询开发指南中的包管理器，固定检索已经足够。如果用户要求解释一段登录流程，而资料散落在几个文件中，助手可能要先找入口，再追到具体实现。后者更适合逐步观察和行动。</p><h2 id="">每一轮应该给模型看什么</h2><p>一次请求里通常装着系统规则、用户问题、最近消息、工具说明和检索结果。这些内容共同占用上下文。</p><p>把整个项目全塞进去，会增加输入量，也会夹进大量与当前问题无关的内容。只给一小段，又可能缺少决定答案的前提。后面讲的记忆和检索，都与这个取舍有关。</p><p>先保留眼下最需要的材料。用户问启动方式，就优先提供开发指南和包管理配置；问某个字段的来源，再去找相关代码。每次补充内容，都应该能解释它为什么与当前判断有关。</p><p>历史记录也要按这个原则使用。数据库里保存了一万条消息，只能说明应用存过它们。最终请求实际装了哪些消息，仍由程序决定。</p><h2 id="">怎样确认任务完成了</h2><p>模型写出“已经完成”只是一个信号。文件任务要检查文件，代码任务要检查相关行为，知识问答要检查答案和证据。</p><p>停止原因还要分开记录。正常完成、工具失败和达到执行上限，对用户意味着不同结果。让循环最多运行五轮，可以避免无休止地执行；第五轮停下来时，任务可能仍未做完。</p><p>这一篇可以做一个纸面练习。把“告诉我项目怎样启动”写成一次执行记录，依次标出模型请求、工具的真实输出和最终答案。然后删掉工具输出，看你还能凭什么确定答案。这一步想清楚以后，再看框架封装的函数会容易很多。</p><h2 id="">延伸阅读</h2><p>本篇依据冴羽《AI Agent 开发实战》学习整理稿中的概念总览与最小执行机制重写。原教程见冴羽的 <a href="https://articles.zsxq.com/id_c94azm57grb9.html">Agent 基本概念</a>、<a href="https://articles.zsxq.com/id_ihw7khu8jozx.html">JavaScript 最小实现</a> 和 <a href="https://articles.zsxq.com/id_indsj80wol1c.html">先动手的学习建议</a>。</p><p>系列下一篇会写一个无需模型密钥的离线循环，让请求、执行和回传都能在终端里看见。</p><a id="series"></a><h2 id="">系列目录</h2><ol start="1"><li><a href="https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does">AI Agent 开发 01｜先弄清楚，谁在执行任务</a></li><li><a href="https://www.amfishers.com/posts/technology/ai-agent-02-build-the-loop">AI Agent 开发 02｜用一个离线例子看清执行循环</a></li><li><a href="https://www.amfishers.com/posts/technology/ai-agent-03-react-and-planning">AI Agent 开发 03｜任务走不动时，该继续试还是重新规划</a></li><li><a href="https://www.amfishers.com/posts/technology/ai-agent-04-tools-and-mini-cursor">AI Agent 开发 04｜把工具写清楚，助手才有办法把事做完</a></li><li><a href="https://www.amfishers.com/posts/technology/ai-agent-05-frameworks-and-workflows">AI Agent 开发 05｜选框架之前，先看你准备省掉哪部分代码</a></li><li><a href="https://www.amfishers.com/posts/technology/ai-agent-06-memory-and-conversations">AI Agent 开发 06｜多轮对话能接上，靠的是怎样保存和取回信息</a></li><li><a href="https://www.amfishers.com/posts/technology/ai-agent-07-mcp-connections">AI Agent 开发 07｜一个工具接到多个助手里，MCP 帮你统一哪部分工作</a></li><li><a href="https://www.amfishers.com/posts/technology/ai-agent-08-rag-and-evidence">AI Agent 开发 08｜资料里明明有答案，RAG 为什么还会答错</a></li><li><a href="https://www.amfishers.com/posts/technology/ai-agent-09-loaders-and-chunks">AI Agent 开发 09｜先把资料读对，再讨论每块切多大</a></li><li><a href="https://www.amfishers.com/posts/technology/ai-agent-10-storage-and-shipping">AI Agent 开发 10｜把知识库助手上线，最后还要验证哪些事</a></li></ol><p><a href="https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does#series">系列目录</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-02-build-the-loop">下一篇</a></p></div><p style="text-align:right"><a href="https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does</link><guid isPermaLink="true">https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does</guid><dc:creator><![CDATA[Fisher]]></dc:creator><pubDate>Thu, 29 Oct 2026 15:52:55 GMT</pubDate></item><item><title><![CDATA[AI Agent 开发 02｜用一个离线例子看清执行循环]]></title><description><![CDATA[<div><blockquote>This render is generated by the Shiro API and may have formatting issues. For the best experience, visit:<a href="https://www.amfishers.com/posts/technology/ai-agent-02-build-the-loop">https://www.amfishers.com/posts/technology/ai-agent-02-build-the-loop</a></blockquote><div><p>先把模型换成一段确定的程序，Agent 的执行过程就很容易观察。我们让它先请求查询项目的启动命令，收到工具结果以后再回答。这个例子不会调用真实模型，也没有语言理解能力，用来检查程序有没有把几个步骤正确连起来。</p><p>准备一个支持 ES 模块和 <code>Array.prototype.at()</code> 的 Node.js 环境。新建 <code>agent-loop.mjs</code>，保存下面的完整代码。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">import { pathToFileURL } from &quot;node:url&quot;;

const tools = new Map([
  [&quot;lookup_command&quot;, async (args) =&gt; {
    if (!args || args.project !== &quot;demo&quot;) {
      throw new Error(&quot;只允许查询 demo 项目&quot;);
    }
    return { command: &quot;pnpm dev&quot;, source: &quot;演示开发指南&quot; };
  }],
]);

export async function demoModel(messages) {
  const last = messages.at(-1);
  if (last.role !== &quot;tool&quot;) {
    return {
      type: &quot;tool_call&quot;, id: &quot;call-1&quot;,
      name: &quot;lookup_command&quot;, args: { project: &quot;demo&quot; },
    };
  }
  const result = JSON.parse(last.content);
  return result.ok
    ? { type: &quot;final&quot;, text: result.data.command }
    : { type: &quot;failed&quot;, text: result.error };
}

export async function runAgent(question, model = demoModel, maxTurns = 4) {
  if (!Number.isInteger(maxTurns) || maxTurns &lt; 1) {
    throw new Error(&quot;maxTurns 必须是正整数&quot;);
  }
  const messages = [{ role: &quot;user&quot;, content: question }];
  const trace = [];

  for (let turn = 0; turn &lt; maxTurns; turn++) {
    const decision = await model(messages);
    if (!decision || typeof decision !== &quot;object&quot;) {
      throw new Error(&quot;模型返回了无效决策&quot;);
    }
    if ([&quot;final&quot;, &quot;failed&quot;].includes(decision.type)) {
      if (typeof decision.text !== &quot;string&quot; || !decision.text.trim()) {
        throw new Error(&quot;结束决策缺少回答&quot;);
      }
      return {
        status: decision.type === &quot;final&quot; ? &quot;completed&quot; : &quot;failed&quot;,
        answer: decision.text, trace,
      };
    }
    if (decision.type !== &quot;tool_call&quot; ||
        typeof decision.id !== &quot;string&quot; || !decision.id.trim() ||
        typeof decision.name !== &quot;string&quot; || !decision.name.trim()) {
      throw new Error(&quot;工具请求格式错误&quot;);
    }

    messages.push({ role: &quot;assistant&quot;, decision });
    let observation;
    try {
      const execute = tools.get(decision.name);
      if (!execute) throw new Error(&quot;未知工具&quot;);
      observation = { ok: true, data: await execute(decision.args) };
    } catch (error) {
      observation = {
        ok: false,
        error: error instanceof Error ? error.message : String(error),
      };
    }
    trace.push({ name: decision.name, observation });
    messages.push({
      role: &quot;tool&quot;, tool_call_id: decision.id,
      content: JSON.stringify(observation),
    });
  }
  return { status: &quot;limit_reached&quot;, answer: &quot;任务尚未完成&quot;, trace };
}

if (process.argv[1] &amp;&amp;
    import.meta.url === pathToFileURL(process.argv[1]).href) {
  console.log(await runAgent(&quot;demo 项目怎样启动？&quot;));
}</code></pre><p>进入文件所在目录，运行它。</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">node agent-loop.mjs</code></pre><p>正常结果里会出现 <code>status: &#x27;completed&#x27;</code>，回答为 <code>pnpm dev</code>。展开 <code>trace</code>，可以看到一次 <code>lookup_command</code> 的调用。命令是演示资料中的固定数据，不能据此启动你的真实项目。</p><h2 id="">第二轮为什么有答案</h2><p>第一轮，<code>demoModel</code> 只会提出一个工具请求。<code>runAgent</code> 从 Map 中找到对应函数，等待执行，再追加一条工具消息。</p><p>第二轮，模型函数看到最后一条消息的角色是 <code>tool</code>，解析结果，输出最终回答。两轮之间传递的就是程序维护的 <code>messages</code>。换成真实模型以后，模型本身不会替你维护这份数组。</p><p>消息中的调用 ID 也有用。<code>call-1</code> 把请求与响应对应起来。当一次模型响应提出多个工具调用时，程序要保留每条请求，并为它们分别提供对应结果。这里为了便于阅读，每轮只处理一个调用。</p><h2 id="">把成功路径故意弄坏</h2><p>先把工具参数中的 <code>project</code> 改成别的字符串。工具会拒绝，下一轮返回 <code>failed</code>。程序得到了真实失败，回答就应该保留这个状态。</p><p>再把工具名改成 <code>missing_tool</code>。Map 查不到它，会产生“未知工具”的观察结果。模型输出的名称必须经过工具注册表检查，不能直接成为任意函数调用。</p><p>最后只允许一轮执行。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">console.log(await runAgent(&quot;demo 项目怎样启动？&quot;, demoModel, 1));</code></pre><p>这时工具已经执行，模型还没获得第二次生成答案的机会，返回值应当是 <code>limit_reached</code>。它恰好说明执行上限与完成状态之间的区别。</p><p>如果模型函数返回 <code>null</code> 或者缺少调用 ID，程序直接抛出格式错误。这类错误值得在模型适配层尽早发现。工具执行失败可以交回模型处理，损坏的协议结构需要先修好。</p><h2 id="">接真实模型时需要补哪部分</h2><p>这份代码里的 <code>decision</code> 是教学协议。你需要写一个适配器，把消息转换成所用 SDK 要求的格式，再把 SDK 返回的工具请求和最终文本转换回来。</p><p>适配器还要保留工具调用标识，处理模型请求失败，并遵循供应商对消息顺序的要求。工具的参数校验和业务权限仍留在执行侧。即使使用结构化工具调用，程序也得判断这次操作是否允许发生。</p><p>这个演示还没有超时、取消和持久化。接入网络工具后，每次请求都应该有明确的超时；服务需要取消任务时，也要把取消信号传给实际执行的函数。轮数上限只能控制调用次数，无法打断一次永久等待的请求。</p><h2 id="">延伸阅读</h2><p>本篇依据冴羽《AI Agent 开发实战》学习整理稿中的离线循环重新编写，正常完成与失败路径均可独立验证。原教程见 <a href="https://articles.zsxq.com/id_t23f5wwr5jkh.html">JavaScript ReAct 实现</a> 和 <a href="https://articles.zsxq.com/id_plwwct29p7w2.html">Python ReAct 实现</a>。Python 版改变了语法，执行、等待和回传的职责保持一致。</p><p><a href="https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does">上一篇</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does#series">系列目录</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-03-react-and-planning">下一篇</a></p></div><p style="text-align:right"><a href="https://www.amfishers.com/posts/technology/ai-agent-02-build-the-loop#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://www.amfishers.com/posts/technology/ai-agent-02-build-the-loop</link><guid isPermaLink="true">https://www.amfishers.com/posts/technology/ai-agent-02-build-the-loop</guid><dc:creator><![CDATA[Fisher]]></dc:creator><pubDate>Thu, 29 Oct 2026 15:52:54 GMT</pubDate></item><item><title><![CDATA[AI Agent 开发 03｜任务走不动时，该继续试还是重新规划]]></title><description><![CDATA[<div><blockquote>This render is generated by the Shiro API and may have formatting issues. For the best experience, visit:<a href="https://www.amfishers.com/posts/technology/ai-agent-03-react-and-planning">https://www.amfishers.com/posts/technology/ai-agent-03-react-and-planning</a></blockquote><div><p>循环能运行以后，下一个问题很实际。每轮都让模型自由决定，会不会一直绕圈？先列一份计划，又能不能应付做到一半才发现的新问题？</p><p>ReAct 和 Plan-and-Execute 给出了两种组织办法。前者随着工具反馈决定行动，后者显式保存计划，再根据执行结果调整。它们都需要真实观察，也都需要停止条件。</p><h2 id="">先观察，再决定下一步</h2><p>原教程用修复程序一类任务解释 ReAct。开始时只知道页面有问题，具体原因还没有定位。先读报错，找到组件，再检查初始化过程。每次行动所需的信息，来自上一次结果。</p><p>把这个方式用到知识库助手，过程也一样。用户问登录以后为什么会跳回首页，助手先检索登录文档。如果文档只说明正常流程，就需要继续查看回调配置；回调中出现新的路径，再检查路由规则。</p><p>这里的示例是任务设计。能否作出最后判断，要看程序实际找到了哪些证据。只要工具结果没有支持结论，助手就应该继续查或说明缺口。</p><p>ReAct 记录里最有用的内容，是行动名称、参数、执行结果和下一步状态。调试循环时，不必强制模型公开完整内部推理。简短的行动说明足够帮助用户理解进度，正确与否仍要用文件、日志和检查结果验证。</p><p>自由行动的困难是局部判断容易重复。工具连续几次返回相同内容，程序就该识别重复状态；缺少权限或凭证时，再试一次通常也不会多得到信息。把“是否可以重试”作为工具结果的一部分，会比让模型从一句报错里猜更清楚。</p><h2 id="">长任务需要保存全局位置</h2><p>Plan-and-Execute 会先把任务分成步骤。原教程区分 Planner、Executor 和 Replanner，分别负责制定计划、执行当前步骤和调整剩余安排。</p><p>以项目知识库助手为例，可以先确认资料目录，再读取内容，接着组织答案，最后核对引用。执行到读取资料时，如果发现一个关键文件缺失，后面的计划就需要改变。</p><p>已经读到的内容应当留下。重规划时只调整尚未完成的部分，避免把前面的工作再做一次。</p><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">// 自定义任务状态，用于说明需要保存什么
type TaskState = {
  goal: string;
  completed: Array&lt;{ step: string; evidence: string[] }&gt;;
  remaining: string[];
  status: &quot;running&quot; | &quot;completed&quot; | &quot;failed&quot; | &quot;limit_reached&quot;;
};</code></pre><p>执行器收到当前步骤时，还需要原始目标、必要的已完成结果和限制条件。只给它“整理引用”，它不知道哪些资料来自本次检索，也不知道最终文章要求引用到什么程度。</p><p>计划中的步骤也要能判断完成。把“研究一下”改成“找到开发指南，记录来源与更新时间”，执行器才能交回可核对的东西。</p><h2 id="">计划什么时候值得调整</h2><p>发现新的依赖、原先假设失效或某一步已经无法继续时，需要重规划。一次普通工具调用成功以后，可以直接进入下一步。每做一个小动作都再调用模型审查整份计划，会增加延迟和费用。</p><p>两种方法可以组合。外面保存一份阶段计划，里面用 ReAct 查清某个未知问题。例如“核对启动方式”是计划中的一步，执行时可能要先读文档，再查包配置，最后比较两者是否一致。</p><p>这也解释了为什么不能只凭模式名称判断成本。计划要生成，工具要执行，必要时还要重规划。总费用与模型选择、上下文大小及调用次数有关。拆成更多角色以后，信息传递本身也需要工作。</p><h2 id="">反思要有一个检查对象</h2><p>让模型再检查一遍，有时能够发现遗漏。它也可能重复原来的错误。检查最好有外部标准。</p><p>知识问答可以核对引用 ID 是否来自本次检索，以及结论有没有被对应片段支持。代码任务可以运行针对改动的检查。固定输出则可以先验证 schema。</p><p>这些检查各自覆盖一部分问题。字段齐全只能证明结构符合要求，不能证明内容正确；引用存在，也不能证明引用支持那句话。把结构检查与内容判断分别记录，故障才容易定位。</p><p>可以给自己设计一次练习。把知识库中的关键文件暂时移走，观察助手会不会调整计划；再给它一个不存在于资料中的问题，观察它能否停止并说明缺少依据。计划最后应保留已经完成的步骤，同时明确指出剩余部分为什么无法完成。</p><h2 id="">延伸阅读</h2><p>本篇依据冴羽《AI Agent 开发实战》学习整理稿中的执行模式与工程补充重写。原教程见 <a href="https://articles.zsxq.com/id_e5nv5d4rjg9j.html">Agent 常见设计模式</a>、<a href="https://articles.zsxq.com/id_8a54rtkkp6wk.html">ReAct 原理</a>、<a href="https://articles.zsxq.com/id_0hxej9m8ogmg.html">计划与执行原理</a> 和 <a href="https://articles.zsxq.com/id_1zors43w227w.html">计划执行的 JavaScript 实现</a>。</p><p><a href="https://www.amfishers.com/posts/technology/ai-agent-02-build-the-loop">上一篇</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does#series">系列目录</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-04-tools-and-mini-cursor">下一篇</a></p></div><p style="text-align:right"><a href="https://www.amfishers.com/posts/technology/ai-agent-03-react-and-planning#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://www.amfishers.com/posts/technology/ai-agent-03-react-and-planning</link><guid isPermaLink="true">https://www.amfishers.com/posts/technology/ai-agent-03-react-and-planning</guid><dc:creator><![CDATA[Fisher]]></dc:creator><pubDate>Thu, 29 Oct 2026 15:52:53 GMT</pubDate></item><item><title><![CDATA[AI Agent 开发 04｜把工具写清楚，助手才有办法把事做完]]></title><description><![CDATA[<div><blockquote>This render is generated by the Shiro API and may have formatting issues. For the best experience, visit:<a href="https://www.amfishers.com/posts/technology/ai-agent-04-tools-and-mini-cursor">https://www.amfishers.com/posts/technology/ai-agent-04-tools-and-mini-cursor</a></blockquote><div><p>同样是读取文件，一个工具只返回“失败”，另一个能说明文件不存在、请求路径是什么、是否可以换一个位置继续查，后面的执行会差很多。工具输出决定了模型接下来有多少依据。</p><p>原教程用 Mini Cursor 说明这件事。最小代码助手可以从列目录、读文件、写文件和运行命令开始。四种能力已经能组成“查看项目、修改代码、执行检查、根据结果继续修正”的过程。</p><p>知识库助手可以先缩小范围，只提供列资料目录和读取资料。等确实需要编辑文件，再加入写入能力。每增加一种工具，都应该知道它会改变什么。</p><h2 id="">工具说明要让模型能作选择</h2><p>工具通常需要名称、描述、输入结构和执行函数。名称用于识别，描述帮助模型判断何时调用，输入结构说明参数，执行函数负责实际工作。</p><p><code>query</code> 这个名字很难让人知道能查什么。<code>lookup_order_delivery</code> 加上“根据订单号查询物流状态”的描述，会清楚得多。如果它只接受订单号，也该直接写明，避免模型尝试传手机号。</p><p>参数说明同样应该具体。路径是相对哪个目录，编码是否限定为 UTF-8，结果会不会截断，都影响调用方式。工具说明接近一份短 API 文档，模糊处会直接成为运行时的问题。</p><h2 id="">参数正确以后，还要检查操作是否允许</h2><p>输入 schema 可以验证 <code>orderId</code> 是非空字符串。程序仍要检查当前用户是否有权读取该订单。结构校验和业务权限分别处理，错误才容易说明。</p><p>文件工具也有类似问题。把 <code>./docs/</code> 加到用户传入路径前面，无法自动拦住 <code>../</code>。应当解析目标路径，验证它确实位于允许目录；存在符号链接时，还要检查解析后的真实目标。</p><p>允许目录应来自服务端配置。让模型自行声明“本次允许访问整个磁盘”，没有增加可信依据。工具读取文件的进程也应拥有与任务相符的系统权限。</p><h2 id="">返回值需要帮助下一轮行动</h2><p>可以给工具设计一个稳定的结果结构。下面是自定义示意，字段要根据业务调整。</p><pre class="language-json lang-json"><code class="language-json lang-json">{
  &quot;ok&quot;: false,
  &quot;code&quot;: &quot;FILE_NOT_FOUND&quot;,
  &quot;retryable&quot;: false,
  &quot;message&quot;: &quot;请求的开发指南不存在&quot;
}</code></pre><p><code>retryable</code> 表示原样重试有没有意义。路径错误时，助手可以回到目录工具找线索；权限不足时，继续换名字读取也不该成为默认做法。</p><p>运行命令时，成功结果也要保留必要的标准输出。启动日志里出现了端口，工具却只返回“执行成功”，模型就无法知道接下来该访问哪个地址。失败时则要带退出码和经过截断、脱敏的关键错误信息。</p><p>输出上限值得提前加。一次命令打印几万行日志，会挤占后面的上下文。工具可以返回摘要、日志定位和截断标记，让模型按需继续读取。</p><h2 id="">命令怎样执行，决定了很多边界</h2><p>原教程中的命令工具用空格拆字符串来简化实现。遇到带空格的路径或引号参数，这种拆法会出错。</p><p>更稳妥的接口把可执行程序和参数数组分开。若任务只需要检查项目，还可以只接受已经定义好的检查名称，由程序映射到明确命令。这样不必把任意文本交给 shell 解释。</p><p>命令执行要设置超时，记录退出码，并保留取消能力。网络中断或子进程卡住时，调用次数再少也可能一直等下去。</p><p>开发服务器还要单独处理。测试命令通常会退出，服务进程则持续运行。如果工具等到进程结束才返回，Agent 就无法进入下一步。需要记录进程状态，检查启动是否成功，然后返回地址与进程标识。</p><h2 id="">写入成功必须有证据</h2><p>异步写文件要等待完成后再返回。原教程的部分教学实现省略了等待，直接用于真实流程时，后一步可能读到旧内容，或者在写入失败以后仍宣称成功。</p><p>对关键修改，可以记录修改前的版本，完成后再读取目标或运行相关检查。验证方式应与用户目标对应。文件存在，只能证明有这个文件；内容和行为是否正确还要继续核对。</p><p>可以用一个受控练习结束这一篇。让助手读取一个现有文件，再让它请求不存在的路径、越界路径和超长日志。检查每次结果有没有清楚区分成功、拒绝和失败。四种基本工具的边界弄清以后，再接更丰富的服务会更踏实。</p><h2 id="">延伸阅读</h2><p>本篇依据冴羽《AI Agent 开发实战》学习整理稿中的工具设计与 Mini Cursor 章节重写。原教程见 <a href="https://articles.zsxq.com/id_hk4zmz56b7fx.html">Mini Cursor 实现</a> 和 <a href="https://articles.zsxq.com/id_xmwq3hr1c50h.html">Mastra 工具入门</a>。</p><p><a href="https://www.amfishers.com/posts/technology/ai-agent-03-react-and-planning">上一篇</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does#series">系列目录</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-05-frameworks-and-workflows">下一篇</a></p></div><p style="text-align:right"><a href="https://www.amfishers.com/posts/technology/ai-agent-04-tools-and-mini-cursor#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://www.amfishers.com/posts/technology/ai-agent-04-tools-and-mini-cursor</link><guid isPermaLink="true">https://www.amfishers.com/posts/technology/ai-agent-04-tools-and-mini-cursor</guid><dc:creator><![CDATA[Fisher]]></dc:creator><pubDate>Thu, 29 Oct 2026 15:52:52 GMT</pubDate></item><item><title><![CDATA[AI Agent 开发 05｜选框架之前，先看你准备省掉哪部分代码]]></title><description><![CDATA[<div><blockquote>This render is generated by the Shiro API and may have formatting issues. For the best experience, visit:<a href="https://www.amfishers.com/posts/technology/ai-agent-05-frameworks-and-workflows">https://www.amfishers.com/posts/technology/ai-agent-05-frameworks-and-workflows</a></blockquote><div><p>手写一次循环以后，框架的作用会具体很多。模型接入、工具消息、流式返回和状态保存都需要维护。业务增加以后，把这些通用部分反复写一遍，会占掉不少时间。</p><p>选框架可以从当前缺口出发。知识库助手眼下需要的是接入模型、定义检索工具，以及把答案和引用返回给页面。先把这条路径跑通，再看恢复执行或复杂分支是否真的需要加入。</p><h2 id="">几个名字各自负责什么</h2><p>原教程同时介绍了 Mastra 和 LangChain.js。Mastra 把 Agent、Workflow、Memory 和调试能力放在同一套 TypeScript 开发体验中。LangChain 提供模型、工具和 Agent 的组合接口，需要更细的状态控制时可以继续使用 LangGraph。追踪和评估则可以放到 LangSmith 一类工具里。这些职责可以从 <a href="https://mastra.ai/docs/agents/overview">Mastra 文档</a> 与 <a href="https://docs.langchain.com/oss/javascript/langchain/overview">LangChain 文档</a> 中核对。</p><p>选择时最好拿自己的任务试一遍。能否连接当前使用的模型，部署环境是否支持依赖，工具失败后在哪里能看见结果，这些问题会比下载量更早影响项目。</p><p>前端交互占主要工作时，也可以评估 AI SDK 的模型与消息流能力。已有后端则先检查能否直接嵌入。为了一个小型问答功能另起完整服务，会增加部署和维护工作。</p><h2 id="-mastra--agent--workflow">用 Mastra 理解 Agent 与 Workflow</h2><p>原教程创建 Agent 时，先设置稳定标识、展示名称、行为指令和模型，再挂上工具与记忆。这里可以沿用普通模块的理解方式，先定义对象，再注册到应用入口。</p><p>开放任务由 Agent 依据结果选择行动。必须按规则经过的步骤，可以交给 Workflow。</p><p>例如生成文章以后再翻译，流程里可以有两个 Step。第一步接收主题，返回 <code>{ article }</code>；第二步接收文章，返回 <code>{ translated }</code>。一次实际执行就是一个 Run。</p><p>这几个对象要靠真实字段接起来。如果整个工作流声明返回 <code>{ result }</code>，最后一步却只交回 <code>{ translated }</code>，就需要统一名称或显式映射。schema 可以描述契约，实际输出还得遵守契约。</p><p>知识库助手也可以设计成两个固定步骤。先检索并生成答案，再检查引用 ID 是否来自本次结果。检查不通过时，返回明确错误或者重新生成。至于要不要允许再次检索，需要在流程里规定。</p><h2 id="">组合组件之前，先看数据形状</h2><p>LangChain 的 Runnable 接口让组件能以相近方式调用。模型、模板和解析器因此更容易组合。它们的输入输出类型仍可能不同。</p><p>原教程中一个容易忽略的情况，是上一步已经返回字符串，下一步模板却需要 <code>{ translated }</code>。中间需要映射。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">// 自定义映射示意，展示字段怎样接上
const translated = &quot;translated text&quot;;
const nextInput = { translated };</code></pre><p>同样的问题会出现在检索和生成之间。检索器可能返回 Document 数组，生成模板需要整理好的资料文本。程序应该明确选择哪些字段参与上下文，哪些字段保留给引用显示。</p><p>每个步骤先打印一次输入类型和输出结构，通常就能发现字段接错。等流程稳定以后，再用适当的日志记录这些信息。真实用户资料和凭证应当脱敏，避免调试输出变成额外的数据副本。</p><h2 id="">调试界面能帮你确认到哪一步</h2><p>Studio 或调用追踪能回答一类很有价值的问题。工具究竟返回了错误数据，还是模型没有正确使用它？</p><p>如果检索工具已经拿错片段，就先修检索；如果片段正确，答案却遗漏条件，再看上下文和生成阶段。把一次运行展开，比只看最终那句话更容易找到修改位置。</p><p>开发调试界面与产品页面承担不同任务。工具在 Studio 中能运行以后，还需要自己的接口、用户状态、错误展示和部署配置。到这一阶段，框架示例只能证明部分能力已经连通。</p><p>具体 API 会随版本变化。这篇用职责和数据契约解释框架，不把结构示意当作可直接复制的安装模板。真正练习时，固定一组依赖，保留锁文件，再按对应版本文档补齐调用。</p><p>试着用你选的一套框架重做上一篇的文件读取工具。记录哪些循环代码被框架接管，哪些权限判断仍由自己维护。能把这两部分指清楚，框架就已经帮上忙了。</p><h2 id="">延伸阅读</h2><p>本篇依据冴羽《AI Agent 开发实战》学习整理稿中的框架、Mastra 工作流与 Runnable 章节重写。原教程见 <a href="https://articles.zsxq.com/id_i3nvkwrjmxub.html">框架选型</a>、<a href="https://articles.zsxq.com/id_xmwq3hr1c50h.html">Mastra 入门</a>、<a href="https://articles.zsxq.com/id_ac9vbq7t7apo.html">LangChain.js 入门</a> 和 <a href="https://articles.zsxq.com/id_z3jr228w41sp.html">LangChain.js 核心概念</a>。</p><p><a href="https://www.amfishers.com/posts/technology/ai-agent-04-tools-and-mini-cursor">上一篇</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does#series">系列目录</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-06-memory-and-conversations">下一篇</a></p></div><p style="text-align:right"><a href="https://www.amfishers.com/posts/technology/ai-agent-05-frameworks-and-workflows#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://www.amfishers.com/posts/technology/ai-agent-05-frameworks-and-workflows</link><guid isPermaLink="true">https://www.amfishers.com/posts/technology/ai-agent-05-frameworks-and-workflows</guid><dc:creator><![CDATA[Fisher]]></dc:creator><pubDate>Thu, 29 Oct 2026 15:52:51 GMT</pubDate></item><item><title><![CDATA[AI Agent 开发 06｜多轮对话能接上，靠的是怎样保存和取回信息]]></title><description><![CDATA[<div><blockquote>This render is generated by the Shiro API and may have formatting issues. For the best experience, visit:<a href="https://www.amfishers.com/posts/technology/ai-agent-06-memory-and-conversations">https://www.amfishers.com/posts/technology/ai-agent-06-memory-and-conversations</a></blockquote><div><p>第一轮告诉助手项目用 pnpm，第二轮问“那我应该运行什么命令”，应用得把前面的信息带回请求。只发送第二句话，模型就缺少“那”指向的内容。</p><p>对话记忆由应用保存、选择，再提供给模型。弄清这三个动作，才知道信息为什么会遗失，或者为什么会出现在不该出现的会话里。</p><h2 id="">最近消息先解决眼前的追问</h2><p>最直接的做法是保存消息历史，每次请求带上最近一部分。当前会话连续讨论同一个项目时，这已经很有用。</p><p>历史继续增长以后，输入会变长，也会夹进已经结束的话题。此时需要决定保留哪些原文，哪些内容可以压缩。这个决定应该根据后续任务需要什么来做。</p><p>知识库助手刚读过一个接口定义，用户接着问其中某个字段，可以保留对应片段和最近问题。几小时前讨论过的界面配色，通常没有必要一直跟着这次请求。</p><h2 id="">摘要能节省空间，也会遗漏细节</h2><p>原教程介绍 Mastra 的观察记忆，用 Observer 把长对话整理成观察笔记，再由 Reflector 合并和提炼积累的内容。</p><p>摘要适合保留阶段决定和话题关系。精确字段名、执行结果及尚未解决的约束，则需要能回查原文或结构化状态。把“只在测试环境运行”压成“已经确认可以运行”，后面的行动就可能越过原本范围。</p><p>压缩后可以抽样核对。原来明确要求保留的限制有没有留下，未完成的任务有没有被写成完成，谁提出的判断有没有被混在一起。摘要本身也是一次生成结果，需要检查。</p><h2 id="">用户资料与当前任务分开保存</h2><p>工作记忆可以是一份持续更新的资料。默认使用中文、主要写 TypeScript 这类偏好，可能跨会话仍有帮助。某个项目今天使用的启动命令，则应该跟项目绑定。</p><p>原教程用 <code>resourceId</code> 和 <code>threadId</code> 区分资源与会话范围。理解它们时，可以先看信息允许被哪些请求共享，再映射到具体框架的配置。</p><pre class="language-text lang-text"><code class="language-text lang-text">用户 A
  会话 1 讨论项目甲
  会话 2 讨论项目乙

用户 B
  会话 3 讨论自己的项目</code></pre><p>用户 A 的语言偏好可以在合适配置下共享。项目甲尚未解决的错误，不该自动变成项目乙的背景。用户 B 更不能看到用户 A 的资料。</p><p>这些 ID 只负责定位。服务端仍要验证当前登录用户是否有权访问会话。浏览器传来一个 thread ID，无法单凭这个值证明归属。</p><h2 id="">旧话题需要另一种找法</h2><p>最近消息适合追问眼前的内容。如果用户想找三个月前讨论的分块方案，只取最近十条消息通常找不到。语义召回可以从保存的历史中寻找相关内容，再把其中一部分带进来。</p><p>召回结果仍要保留时间和来源。旧项目的决定可能已经过期，语义相近也可能属于另一份文档。把召回结果直接当成当前事实，会混淆时间和适用范围。</p><p>不必一开始就加入所有记忆形式。先让同一会话的追问接得上，再保存明确需要跨会话的偏好。长对话出现实际成本时再压缩；确实要找旧话题时再做语义召回。</p><h2 id="">存进数据库以后，还要验证能不能恢复</h2><p>保存与召回是不同动作。数据库中有记录，并不意味着请求已经使用了它。需要检查写入是否成功、查询条件是否正确，以及最终上下文里有没有对应内容。</p><p>原教程的 LangChain 示例使用内存型 checkpointer。它可以在同一进程里保留状态，进程重启以后就会丢失。需要长期运行时，要选择持久化存储，并测试断开、重连和重启后的恢复。</p><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><h2 id="">延伸阅读</h2><p>本篇依据冴羽《AI Agent 开发实战》学习整理稿中的四类记忆、会话范围与持久化说明重写。原教程见 <a href="https://articles.zsxq.com/id_oxenjphi7n9k.html">Mastra 记忆功能</a> 和 <a href="https://articles.zsxq.com/id_ac9vbq7t7apo.html">LangChain.js 会话示例</a>。</p><p><a href="https://www.amfishers.com/posts/technology/ai-agent-05-frameworks-and-workflows">上一篇</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does#series">系列目录</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-07-mcp-connections">下一篇</a></p></div><p style="text-align:right"><a href="https://www.amfishers.com/posts/technology/ai-agent-06-memory-and-conversations#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://www.amfishers.com/posts/technology/ai-agent-06-memory-and-conversations</link><guid isPermaLink="true">https://www.amfishers.com/posts/technology/ai-agent-06-memory-and-conversations</guid><dc:creator><![CDATA[Fisher]]></dc:creator><pubDate>Thu, 29 Oct 2026 15:52:50 GMT</pubDate></item><item><title><![CDATA[AI Agent 开发 07｜一个工具接到多个助手里，MCP 帮你统一哪部分工作]]></title><description><![CDATA[<div><blockquote>This render is generated by the Shiro API and may have formatting issues. For the best experience, visit:<a href="https://www.amfishers.com/posts/technology/ai-agent-07-mcp-connections">https://www.amfishers.com/posts/technology/ai-agent-07-mcp-connections</a></blockquote><div><p>项目已经有一个查文档的函数。接进第一个助手时，需要写工具说明和调用逻辑；换到另一个应用，可能又要适配一遍。MCP 为这类连接提供共同协议，让客户端能发现服务提供的能力，再按约定调用。</p><p>执行任务的循环仍由应用组织。模型提出工具请求以后，程序决定是否执行，MCP 客户端负责与服务通信，服务返回真实结果。这几个职责分别看清，接入出了问题才容易定位。</p><h2 id="">顺着一次查询走一遍</h2><p>以 <code>search_project_docs</code> 为例，服务声明工具名称、说明和输入 schema。客户端取得这些定义，把适合提供给模型的工具注册进去。</p><p>模型决定查询开发指南时，Agent 程序先验证这次调用，再通过客户端发给服务。服务搜索资料，返回片段与来源。应用把结果放回当前任务，模型才能继续回答。</p><p>MCP 还可以提供资源和提示模板。这里先只讨论工具，因为知识库助手当前需要的是可调用的检索能力。完整能力边界可以查看 <a href="https://modelcontextprotocol.io/docs/learn/architecture">MCP 架构文档</a>。</p><h2 id="">先说明本文使用的协议范围</h2><p>原教程通过初始化、能力发现和调用来解释 MCP。协议会演进，本文用 <strong>2025-11-25 版规范</strong>说明这条连接过程。真实项目应采用客户端与 SDK 支持的兼容版本，不要把这里的版本号视作始终最新。</p><p>按这版规范，客户端先发出 <code>initialize</code>，协商协议版本和能力；服务回应后，客户端发送初始化完成通知，再进入正常操作。<a href="https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle">生命周期规范</a> 给出了具体消息要求。</p><p>随后可以通过 <code>tools/list</code> 读取工具定义，用 <code>tools/call</code> 请求执行。下面只展示一个调用请求。</p><pre class="language-json lang-json"><code class="language-json lang-json">{
  &quot;jsonrpc&quot;: &quot;2.0&quot;,
  &quot;id&quot;: 7,
  &quot;method&quot;: &quot;tools/call&quot;,
  &quot;params&quot;: {
    &quot;name&quot;: &quot;search_project_docs&quot;,
    &quot;arguments&quot;: { &quot;query&quot;: &quot;本地启动命令&quot; }
  }
}</code></pre><p>带 ID 的请求需要用对应 ID 返回响应。通知没有 ID，不要求响应。这里只是消息片段，正式实现还需要处理连接生命周期、错误、取消及协商能力，不能把它直接当成完整服务。</p><h2 id="stdio--http-">stdio 和 HTTP 各自怎样工作</h2><p>stdio 方式通常由客户端启动子进程，通过标准输入输出交换协议消息。工具服务的普通调试日志应写到标准错误，避免混进协议通道。</p><p>Streamable HTTP 则连接一个正在运行的 HTTP 服务，也可以连接本机地址。它与 stdio 的差别在传输方式。身份认证、请求头和连接管理要按所用 SDK 处理。<a href="https://modelcontextprotocol.io/specification/2025-11-25/basic/transports">对应版本的传输规范</a> 解释了两者要求。</p><p>先单独连一次服务，确认能列出工具、能用固定参数取得结果，再交给模型。这会把“连接失败”和“模型没有选对工具”分开。</p><p>如果服务能列出工具，却调用失败，先看参数 schema 和服务端错误。固定调用已经成功，模型仍没使用它，再检查工具说明是否清楚，以及本轮是否真的需要这个能力。</p><h2 id="">适配器减少了哪些工作</h2><p>原教程通过 LangChain 的 MCP 适配器，把服务工具转换成 Agent 能使用的工具。调用侧可以继续使用同一套执行循环，工具的实现则由独立服务维护。</p><p>文件系统服务可以替换代码助手中手写的列目录和文件读取。自己的检查命令工具也可以继续保留。两种来源的能力可以放在同一个助手里，前提是输入输出与权限边界都明确。</p><p>短任务结束后，客户端要按 SDK 的方式释放连接和子进程。长期服务则需要处理连接复用、异常断开和重新发现。是否能重复调用，也要由实际行为验证。</p><h2 id="">连通以后，权限仍要看整体</h2><p>文件服务限制在一个目录中，另一个任意命令工具却可能读取目录之外的内容。评估实际访问范围时，需要检查 Agent 的全部工具，以及这些工具进程拥有的系统权限。</p><p>工具描述也只是能力说明。读取敏感资料或修改文件时，服务端仍应校验用户、资源范围和业务条件。</p><p>这一篇的练习可以很小。先把只读文档搜索封成服务，用固定问题直接调用；记录返回的正文、来源和错误。然后接入助手，再用同一个问题询问。两份结果放在一起，能看清连接和模型使用之间多了哪些步骤。</p><h2 id="">延伸阅读</h2><p>本篇依据冴羽《AI Agent 开发实战》学习整理稿中的 MCP 连接与手写协议章节重写，并核对上述版本规范。原教程见 <a href="https://articles.zsxq.com/id_qxchwdgnckkd.html">MCP 工具接入</a> 和 <a href="https://articles.zsxq.com/id_h7ixnipwvkju.html">从零理解 MCP</a>。</p><p><a href="https://www.amfishers.com/posts/technology/ai-agent-06-memory-and-conversations">上一篇</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does#series">系列目录</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-08-rag-and-evidence">下一篇</a></p></div><p style="text-align:right"><a href="https://www.amfishers.com/posts/technology/ai-agent-07-mcp-connections#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://www.amfishers.com/posts/technology/ai-agent-07-mcp-connections</link><guid isPermaLink="true">https://www.amfishers.com/posts/technology/ai-agent-07-mcp-connections</guid><dc:creator><![CDATA[Fisher]]></dc:creator><pubDate>Thu, 29 Oct 2026 15:52:49 GMT</pubDate></item><item><title><![CDATA[AI Agent 开发 08｜资料里明明有答案，RAG 为什么还会答错]]></title><description><![CDATA[<div><blockquote>This render is generated by the Shiro API and may have formatting issues. For the best experience, visit:<a href="https://www.amfishers.com/posts/technology/ai-agent-08-rag-and-evidence">https://www.amfishers.com/posts/technology/ai-agent-08-rag-and-evidence</a></blockquote><div><p>知识库助手要回答项目自己的问题，得先找到项目资料。RAG 把相关内容取出来，放进模型当前请求，再让模型依据它生成回答。</p><p>这条过程没有改变模型参数。资料更新以后，主要工作是同步来源和索引，再验证查询结果。模型能否答对，还取决于读取、切分、检索和生成是否都处理正确。</p><h2 id="">建索引与回答问题分开运行</h2><p>索引任务先读取原始文件，整理成统一文档，切成片段，编码并保存。查询服务则接收问题，在已有资料中搜索，再组织上下文。</p><pre class="language-text lang-text"><code class="language-text lang-text">索引任务
资料 → 读取与清理 → 分块 → 编码 → 保存

查询服务
问题 → 检索已有数据 → 组织证据 → 生成回答</code></pre><p>教程演示有时把两条过程写在同一个脚本里，便于第一次观察完整结果。长期运行时，每次提问都重读整批文件并编码，会增加耗时，也可能重复写入数据。</p><p>知识库助手应当知道当前查询使用哪个索引版本。更换分块方式或编码模型时，新索引验收以后再切换，可以保留可对照的旧结果。</p><h2 id="">向量解决的是相似内容怎样找</h2><p>Embedding 把文本变成数值向量，用于比较相关程度。提问换了措辞，向量检索仍可能找到描述同一件事的资料。</p><p>问题与文档要使用兼容的编码方式。两个模型输出相同维度，也不能据此混用它们的向量。模型、维度或预处理改变以后，需要评估并相应重建索引。</p><p>相似度分数也有范围。它描述当前编码与检索系统如何排序，不能证明片段包含答案。关于退款的段落可能与价格问题很接近，却没有每年收费的具体数字。</p><p>原教程用余弦相似度解释向量方向的比较。实际选择点积、余弦或欧氏距离时，要符合模型和索引要求。维度更高同样不能单独证明检索质量更好。</p><h2 id="topk-">topK 返回了内容，也可能没有证据</h2><p><code>topK = 3</code> 表示取排名靠前的三条。即使整批资料都没有正确答案，它仍可能返回三条结果。</p><p>因此需要看结果是否支持问题。可以结合相关性判断、去重和重排控制进入上下文的内容，并在资料不足时明确说明缺口。阈值需要用实际问题评估，别把某个分数直接当成通用标准。</p><p>精确标识还值得单独测试。用户搜索字段名 <code>warehouseId</code> 或错误码时，关键词检索可以补充向量检索。两种检索怎样融合，应当固定一组问题比较。<a href="https://docs.langchain.com/oss/javascript/deepagents/retrieval">LangChain 的检索文档</a> 也区分了先检索再生成与由 Agent 决定何时检索的组织方式。</p><h2 id="">把来源一直带到回答里</h2><p>检索片段至少保留正文、资料 ID 和可定位来源。项目制度有多个版本时，再加入版本与有效时间。组织上下文时，让片段编号与来源对应。</p><pre class="language-text lang-text"><code class="language-text lang-text">[资料 1]
文件 development.md，章节 本地启动，版本 v3
项目使用 pnpm，启动命令为 pnpm dev。</code></pre><p>这是教学用资料。生成时可以要求关键结论使用本次资料编号，没有依据时直接说明缺少什么。程序再核对回答引用的编号是否存在。</p><p>编号存在以后，仍要检查内容是否支持结论。某段只说明包管理器，回答却把它引为部署环境的依据，引用格式正确也没有帮助。</p><p>检索材料中的文字还可能夹带操作指令。应用应该把它当成待分析内容，不能让资料里的“忽略前面的要求”覆盖系统规则。</p><h2 id="">沿着实际数据找故障</h2><p>先确认原始文件中确实有答案，再看解析后的文字。如果 PDF 中的表格关系已经丢失，后面换检索模型也无法可靠恢复。</p><p>文字正确，再检查切出的片段。条件与结论是否被分开，章节标题有没有保留，决定最终答案的限制是否完整。片段没有问题，再检查搜索是否召回，过滤是否排掉了正确资料。</p><p>最后才看生成阶段。把真实召回片段单独打印，检查模型是否遗漏前提、误用旧版本或引用了无关内容。检索没找到，与找到以后读错了，需要不同的修复。</p><p>可以准备十个问题，提前为每个问题标出支持答案的原始位置，其中两个明确没有答案。分别记录检索是否命中、回答是否正确、引用是否支持结论。错误留在具体环节，下一次调整才能知道有没有变好。</p><h2 id="">延伸阅读</h2><p>本篇依据冴羽《AI Agent 开发实战》学习整理稿中的索引、向量化和证据核对章节重写。原教程见 <a href="https://articles.zsxq.com/id_uiwwlwj4mb5w.html">RAG 原理</a>、<a href="https://articles.zsxq.com/id_xjv8wzn7bokt.html">企业知识库实践</a> 和 <a href="https://articles.zsxq.com/id_wisw07zezzkh.html">文档向量化与语义检索</a>。</p><p><a href="https://www.amfishers.com/posts/technology/ai-agent-07-mcp-connections">上一篇</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does#series">系列目录</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-09-loaders-and-chunks">下一篇</a></p></div><p style="text-align:right"><a href="https://www.amfishers.com/posts/technology/ai-agent-08-rag-and-evidence#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://www.amfishers.com/posts/technology/ai-agent-08-rag-and-evidence</link><guid isPermaLink="true">https://www.amfishers.com/posts/technology/ai-agent-08-rag-and-evidence</guid><dc:creator><![CDATA[Fisher]]></dc:creator><pubDate>Thu, 29 Oct 2026 15:52:48 GMT</pubDate></item><item><title><![CDATA[AI Agent 开发 09｜先把资料读对，再讨论每块切多大]]></title><description><![CDATA[<div><blockquote>This render is generated by the Shiro API and may have formatting issues. For the best experience, visit:<a href="https://www.amfishers.com/posts/technology/ai-agent-09-loaders-and-chunks">https://www.amfishers.com/posts/technology/ai-agent-09-loaders-and-chunks</a></blockquote><div><p>PDF 里一张价格表被读成连续文字，版本名与价格之间的关系就可能丢掉。后面即使检索到这一段，也很难可靠判断某个价格属于哪个版本。</p><p>做知识库时，先打开 Loader 的实际输出。确认资料读得完整，再往下编码，能更早发现问题。</p><h2 id="">统一文档要保留哪些东西</h2><p>原教程用 Document 组织正文和元数据。正文参与后续文本处理，元数据帮助定位来源、过滤和引用。</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">// 自定义文档示意
const document = {
  pageContent: &quot;项目使用 pnpm 管理依赖。&quot;,
  metadata: {
    docId: &quot;development-guide&quot;,
    source: &quot;docs/development.md&quot;,
    section: &quot;开发环境&quot;,
    version: &quot;v3&quot;,
  },
};</code></pre><p>标题放进 metadata，不代表它会自动参与 embedding。如果希望标题影响语义检索，需要明确把标题加入编码文本，同时保留来源字段。</p><p>逻辑文档 ID 用来更新和删除，片段 ID 用来去重和引用。权限字段则应来自服务端可信记录。上传文件可以带自定义 frontmatter，却不应该自行决定所属租户。</p><h2 id="">不同格式会丢掉不同的信息</h2><p>Markdown 可以分开 frontmatter 和正文，再选择需要的字段写入 metadata。字段要校验，避免任意内容覆盖可信来源。</p><p>FAQ 最好把问题和答案放在一起。答案只有“最多十人”，离开问题以后很难知道限制对象。加入原问题，可以同时保留检索措辞和回答条件。</p><p>PDF 要检查多栏顺序和表格关系。扫描件没有可提取文本层时，需要 OCR。表格可以保留为 Markdown，或者逐行整理成包含列名的句子。只看解析器有没有抛错，还无法知道这些关系是否留下。</p><p>网页则需要检查正文区域。只取段落的选择器可能漏掉标题、代码、列表和表格。动态生成的内容还要确认是否存在于取得的 HTML 中。SaaS 文档同步需要继续处理分页、权限、更新和删除。</p><p>每种来源先抽一份人工比较。原文中的关键字段、条件和章节都能在输出中找到，再继续下一步。</p><h2 id="">分块要让一段内容能够被理解</h2><p>块太大，一个向量混进许多主题，检索粒度变粗。块太小，条件和结论容易散开。合适大小与资料类型和真实问题有关。</p><p>中文资料可以从段落和句末标点开始尝试递归切分。递归分块器先按较粗边界拆开，对过长内容继续细分，再把能放在一起的片段合并。</p><p>因此 <code>chunkSize</code> 通常表示长度目标或限制，具体输出未必每块等长。容量为一百时，两段合起来七十五，下一段放不下，就可能停在七十五。</p><p>只有一个分隔符的切法，遇到没有该分隔符的长文本时还可能产生超长块。要打印最终结果，检查边界与长度，不能只看配置名称。</p><h2 id="overlap-">overlap 要看最终留下了什么</h2><p><code>chunkOverlap</code> 希望让相邻块共享部分内容。某些合并实现会保留完整尾部片段。如果最小尾部片段已经大于重叠目标，它就可能无法留下。</p><p>这解释了为什么设了非零 overlap，实际两块仍可能没有重复文字。原教程专门分析了这种合并行为。最终效果取决于分隔符、片段长度与实现版本。</p><p>重复太多也有成本。同一段内容被多个块召回，会占用上下文，还可能让结果看起来很多、独立证据却很少。调参时应该同时观察重复率。</p><h2 id="">长度单位别混在一起</h2><p>字符长度与 token 数量没有固定换算关系。中文、英文、代码和数字在不同 tokenizer 下会有不同切法。</p><p>按 token 切分更便于控制模型容量，边界仍可能落在句子中间。也可以给递归切分器使用合适的 token 计数函数，再复核最终片段。</p><p>完整请求还要装系统规则、问题、消息历史、工具定义，并为输出留空间。片段没有超限，不能证明整次请求一定装得下。</p><p>Markdown 可以保留章节路径，代码可以在函数附近分开，表格则要重复必要列名。用小片段检索后返回较大的父章节，也是一种选择，但需要维护映射、去重和总长度。</p><p>先拿三组候选块大小在同一批问题上比较。查看正确证据是否被召回，条件是否完整，再看回答质量与输入量。只有发现切分边界确实影响结果时，再增加更复杂的策略。</p><h2 id="">延伸阅读</h2><p>本篇依据冴羽《AI Agent 开发实战》学习整理稿中的 Document、加载与递归分块分析重写。原教程见 <a href="https://articles.zsxq.com/id_glxbp4hvkals.html">Loader 与 Splitter 入门</a>、<a href="https://articles.zsxq.com/id_d19k1h8wvj41.html">文档加载的影响</a> 和 <a href="https://articles.zsxq.com/id_4cnjc52adfce.html">分块器详解</a>。</p><p><a href="https://www.amfishers.com/posts/technology/ai-agent-08-rag-and-evidence">上一篇</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does#series">系列目录</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-10-storage-and-shipping">下一篇</a></p></div><p style="text-align:right"><a href="https://www.amfishers.com/posts/technology/ai-agent-09-loaders-and-chunks#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://www.amfishers.com/posts/technology/ai-agent-09-loaders-and-chunks</link><guid isPermaLink="true">https://www.amfishers.com/posts/technology/ai-agent-09-loaders-and-chunks</guid><dc:creator><![CDATA[Fisher]]></dc:creator><pubDate>Thu, 29 Oct 2026 15:52:47 GMT</pubDate></item><item><title><![CDATA[AI Agent 开发 10｜把知识库助手上线，最后还要验证哪些事]]></title><description><![CDATA[<div><blockquote>This render is generated by the Shiro API and may have formatting issues. For the best experience, visit:<a href="https://www.amfishers.com/posts/technology/ai-agent-10-storage-and-shipping">https://www.amfishers.com/posts/technology/ai-agent-10-storage-and-shipping</a></blockquote><div><p>内存里放几份文档，可以很快演示检索。进程重启后数据没了，多个实例又各自保存一份，存储问题就出现了。</p><p>走到上线这一步，先保证已有数据能持续使用，再检查页面到工具的整条请求。网页能够打开，只覆盖了其中一部分。</p><h2 id="">先把存储需求写具体</h2><p>MemoryVectorStore 适合小样本学习和建立对照。它也可以做过滤，不能因为数据放在内存里就断言它没有过滤能力。需要进一步比较的是持久化、共享、查询执行和更新管理。</p><p>如果项目已有 PostgreSQL，可以把 pgvector 放进候选。它支持精确搜索，也提供 HNSW 和 IVFFlat 等近似索引，具体用法见 <a href="https://github.com/pgvector/pgvector">pgvector 项目文档</a>。是否适合当前任务，还要结合过滤、并发和运维条件验证。</p><p>独立向量服务、混合检索和托管方案也各有候选。原教程介绍了 Qdrant、Weaviate、Milvus、Chroma 和 Pinecone 等方向。选择前先检查部署形态、实际版本与所需能力，用自己的文档和查询比较。</p><p>固定“多少万条以后必须换库”的门槛，无法覆盖向量维度、并发、更新频率和延迟要求。原型的数据量相同，查询条件不同，表现也可能差很多。</p><h2 id="">近似检索省下计算，也引入取舍</h2><p>全量比较会对候选向量逐个计算。ANN 通过索引寻找更有希望的候选，减少一部分计算，也可能漏掉精确搜索能找到的近邻。</p><p>HNSW 使用分层图来导航，搜索先在较稀疏层接近目标区域，再在更密集层继续查。索引构建、内存占用与搜索宽度都需要考虑，不能把它理解成对任何数据都有固定复杂度保证。</p><p>近邻召回与问答证据召回也要分开。向量近邻找得很好，文档本身却没读完整，问题仍然没有依据。性能测试要和前面准备的问题集一起做。</p><h2 id="">迁移以后，查询只连接已有索引</h2><p>把索引任务和查询服务分开。首次编码并写入以后，普通启动应连接已有集合。每次启动都调用会重新编码和写入的方法，可能增加费用，也可能不断产生重复片段。</p><p>稳定文档 ID 与片段版本有助于更新。重复导入同一版本，应按设计保持可控；替换资料以后，旧片段需要清理。删除完成后，再查询一次确认它已经消失。</p><p>正文、元数据和向量要一起迁移。只有向量而取不回正文，模型就没有资料可用。过滤语法也要逐库验证，统一 VectorStore 接口无法抹平底层差异。</p><p>用两个用户的混合资料测试过滤。可信用户范围从服务端认证上下文获得，检索时在正确候选范围内执行。先全库取少量结果，再删掉无权限记录，既可能丢掉有效答案，也可能让不必要的数据进入应用。</p><h2 id="">页面接入要关心任务状态</h2><p>原教程给出独立服务、嵌入已有后端，以及通过 Next.js 服务端接口调用 Agent 的几种方式。根据已有项目选择即可。模型密钥和工具凭证留在服务端，通过环境变量配置。</p><p>前端发送消息，服务端运行 Agent，再把流式事件交回来。流里可能同时包含文本与工具状态，客户端需要使用匹配的消息协议解析。把整个响应当普通文字拼接，可能无法正确显示工具结果。</p><p>页面要处理发送中、接收、结束、错误和取消。后端如果返回 <code>limit_reached</code>，前端应该说明任务尚未完成。连接正常结束，无法单独证明业务目标已经达到。</p><p>生产环境还要验证文件路径、存储卷和运行时。开发机上存在的相对目录，部署包里未必包含；容器能重新启动，也不能证明删除重建后数据仍在。按实际重建方式测试一次，才能知道恢复是否可靠。</p><h2 id="">留一组以后还能重复运行的检查</h2><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><tr><td>工具超时或文件缺失</td><td>返回真实失败状态</td></tr><tr><td>另一个用户的资料</td><td>访问范围隔离正确</td></tr><tr><td>重启并重新连接存储</td><td>已有数据可复用，不重复建库</td></tr></tbody></table><p>为每道题分别记录检索结果、最终回答与引用，再记录延迟和输入输出量。调了分块、换了数据库或升级框架以后，重新跑同一组题，差异才能比较。</p><p>这套系列的第一个可交付版本可以很克制。读取指定项目文档，回答真实问题，保留来源，支持同会话追问，并在失败时如实说明。把这些行为部署后验证一遍，再根据出现的问题增加能力。</p><h2 id="">延伸阅读</h2><p>本篇依据冴羽《AI Agent 开发实战》学习整理稿中的向量数据库、部署与实践验收章节重写。原教程见 <a href="https://articles.zsxq.com/id_81vrcitm16s2.html">向量数据库选型</a>、<a href="https://articles.zsxq.com/id_uk4n1l90rwti.html">Agent 部署</a> 和 <a href="https://articles.zsxq.com/id_6ti2fth0iz7k.html">Next.js 与 Mastra 集成</a>。</p><p><a href="https://www.amfishers.com/posts/technology/ai-agent-09-loaders-and-chunks">上一篇</a> · <a href="https://www.amfishers.com/posts/technology/ai-agent-01-what-an-agent-does#series">系列目录</a></p></div><p style="text-align:right"><a href="https://www.amfishers.com/posts/technology/ai-agent-10-storage-and-shipping#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://www.amfishers.com/posts/technology/ai-agent-10-storage-and-shipping</link><guid isPermaLink="true">https://www.amfishers.com/posts/technology/ai-agent-10-storage-and-shipping</guid><dc:creator><![CDATA[Fisher]]></dc:creator><pubDate>Thu, 29 Oct 2026 15:52:46 GMT</pubDate></item></channel></rss>