<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>潘锦的空间 &#187; Agent</title>
	<atom:link href="https://www.phppan.com/tag/agent/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.phppan.com</link>
	<description>SaaS SaaS架构 团队管理 技术管理 技术架构 PHP 内核 扩展 项目管理</description>
	<lastBuildDate>Sun, 13 Sep 2026 02:08:46 +0000</lastBuildDate>
	<language>zh-CN</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=3.9.40</generator>
	<item>
		<title>DeepSeek Harness 和 Pi 的 Agent Loop 循环结束判断架构细节解析</title>
		<link>https://www.phppan.com/2026/08/deepseek-harness-and-pi-agent-loop-end/</link>
		<comments>https://www.phppan.com/2026/08/deepseek-harness-and-pi-agent-loop-end/#comments</comments>
		<pubDate>Sun, 30 Aug 2026 05:26:34 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[Agent Loop]]></category>
		<category><![CDATA[DeepSeek]]></category>
		<category><![CDATA[DeepSeekHarness]]></category>
		<category><![CDATA[harness]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2533</guid>
		<description><![CDATA[本篇文章代码基线采用 DeepSeek Harness 版本 0.1.2-alpha.1，提交时间为 2026 [&#8230;]]]></description>
				<content:encoded><![CDATA[<section id="nice" data-tool="mdnice编辑器" data-website="https://www.mdnice.com">
<p data-tool="mdnice编辑器">本篇文章代码基线采用 DeepSeek Harness 版本 <code>0.1.2-alpha.1</code>，提交时间为 2026 年 8 月 28 日；Pi 采用 <code>853a80d2</code>。这里的 Pi 指 <code>pi-agent-core</code> 中的官方 Agent Loop。</p>
<p data-tool="mdnice编辑器">就循环究竟在什么时刻结束这个问题，很多 Agent 框架把结束判断写成一个条件：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs"><span class="hljs-keyword">while</span> (hasToolCalls) {
  <span class="hljs-comment">// ...</span>
}
</code></pre>
<p data-tool="mdnice编辑器">在 Demo 中这种循环也够用。进入生产环境后，你会发现会很多问题：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>模型没有调用工具，是否代表任务完成？</section>
</li>
<li>
<section>工具执行完了，模型是否还需要解释结果？</section>
</li>
<li>
<section>用户在工具执行期间发来 steering，应该进入当前轮还是下一轮？</section>
</li>
<li>
<section>输出触达 token 上限后，工具参数还能不能执行？</section>
</li>
<li>
<section>某个工具宣布任务完成，其他并行工具怎么办？</section>
</li>
<li>
<section>Agent 进入 idle 后，Goal Driver 能不能再次唤醒它？</section>
</li>
<li>
<section>Plan mode 退出，是结束当前循环，还是切换下一次请求的策略？</section>
</li>
<li>
<section>进程崩溃后，日志里尚未闭合的 turn 应当如何解释？</section>
</li>
</ul>
<p data-tool="mdnice编辑器">DeepSeek Harness 和 Pi 的工程路线不同，或者说 DeepSeek Harness 更复杂，也更能满足复杂场景的需求。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 将结束拆成多层状态，并把事件日志作为重建依据。Pi 保留了更扁平的循环结构，把 steering、follow-up 和 post-turn stop 暴露成回调。前者适合多插件、持久恢复和长期任务；后者适合轻量嵌入，控制路径也更容易读懂。</p>
<p data-tool="mdnice编辑器">两种实现都能工作。它们承担的系统复杂度不同，结束语义自然不会相同。</p>
<h1 data-tool="mdnice编辑器"><span class="content">五层边界</span></h1>
<p data-tool="mdnice编辑器">讨论 Agent Loop 时，我们需要先定义好「结束」，否则代码里的 <code>completed</code>、<code>turn_end</code>、<code>agent_end</code> 很容易被混为一谈。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 实际存在五层边界。</p>
<p data-tool="mdnice编辑器">第一层是模型请求结束。</p>
<p data-tool="mdnice编辑器">Provider 流式输出完成，可能给出正常停止、工具调用、最大 token、错误或取消。这里的判断逻辑是：当前 HTTP 请求或模型流结束了吗？</p>
<p data-tool="mdnice编辑器">第二层是 step 结束。</p>
<p data-tool="mdnice编辑器">一个 step 包含一次模型调用，以及该响应发起的一整批工具执行。工具执行结束后，step 才算关闭。模型若调用了普通工具，系统通常还欠一次模型回访，因此 step 结束不代表 turn 结束。</p>
<p data-tool="mdnice编辑器">第三层是 turn 结束。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 中，一个 turn 可以包含多个 step，大概如下：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">用户输入
→ 模型调用
→ 工具批次
→ 模型回访
→ 工具批次
→ 模型最终输出
</code></pre>
<p data-tool="mdnice编辑器">以上为一个 turn。turn 结束需要同时满足两项条件：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>模型不再欠一次回复；</section>
</li>
<li>
<section><code>next-step</code> inbox 没有待处理输入。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">第四层是 driver activity 结束。</p>
<p data-tool="mdnice编辑器">一个 driver 可以连续处理多个 turn。当前 turn 关闭后，如果 inbox 里还有普通 follow-up，driver 会直接打开下一 turn。只有队列耗尽，driver 才回到 idle。</p>
<p data-tool="mdnice编辑器">第五层是长期工作流结束。</p>
<p data-tool="mdnice编辑器">Goal Driver 可以监听 idle，检查持久 Goal 状态，再调用 <code>followup()</code> 开启新 turn。Plan mode 也可能跨越多个 turn 持续生效。因此，idle 只描述当前 activity；它无法回答长期目标是否完成。</p>
<p data-tool="mdnice编辑器">Pi 的层级少一些。一次 assistant 响应与随后执行的工具批次，会对应一个 <code>turn_end</code>。工具回访模型时，Pi 再开启下一次 turn。内部队列耗尽后发出 <code>agent_end</code>，当前 invocation 至此结束。</p>
<p data-tool="mdnice编辑器">从语义上看：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>DeepSeek Harness 的 step 接近 Pi 的 turn；</section>
</li>
<li>
<section>DeepSeek Harness 的一次 driver activity 接近 Pi 的一次 Agent Loop invocation；</section>
</li>
<li>
<section>DeepSeek Harness 的 Goal Round 位于 driver activity 外层；</section>
</li>
<li>
<section>Pi 的长期 Goal 通常由宿主或扩展继续调度。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">术语相似，粒度相差一层。如果日志消费端忽略这一点，统计出的 turn 数、工具往返次数和任务完成率都会失真。</p>
<h1 data-tool="mdnice编辑器"><span class="content">驱动收敛</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 的入口：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs"><span class="hljs-keyword">while</span> (<span class="hljs-keyword">await</span> <span class="hljs-keyword">this</span>.turn()) {
}
</code></pre>
<p data-tool="mdnice编辑器"><code>turn()</code> 返回 <code>true</code>，driver 继续处理下一 turn；返回 <code>false</code>，driver 收敛；抛出异常时，异常会被 driver 边界容纳，随后进入 idle。</p>
<p data-tool="mdnice编辑器">短代码背后有一套完整的 phase 状态机：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">idle
maintenance
running
</code></pre>
<p data-tool="mdnice编辑器"><code>running</code> 保存：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>当前 turn；</section>
</li>
<li>
<section>当前 step；</section>
</li>
<li>
<section><code>AbortController</code>；</section>
</li>
<li>
<section><code>wakeRequested</code>。</section>
</li>
</ul>
<p data-tool="mdnice编辑器"><code>maintenance</code> 同样持有取消信号和唤醒锁存，只是对外状态仍显示为 idle。这种设计解决了一个经常被忽略的竞态：Agent 处于维护任务时收到新消息，新消息不能立即启动第二个 driver，也不能静默丢失。系统先将唤醒意图锁存，维护结束后检查 inbox，再决定是否重启。</p>
<p data-tool="mdnice编辑器">取消路径也有类似处理。</p>
<p data-tool="mdnice编辑器">如果一个 waking input 到达时，现有 activity 已经被 abort，这条消息会被重分类到 <code>next-turn</code>。它不能加入一个正在收敛的旧 turn。<code>send()</code> 在插入 inbox 前读取 aborted 状态，避免 inbox observer 触发重入取消后改变分类结果。</p>
<p data-tool="mdnice编辑器">这段细节很像传统并发状态机，而不是常见的聊天循环。它处理的是输入归属问题：一条消息必须属于当前 step、下一 step 或下一 turn，重入事件不能改变已经作出的边界判断。</p>
<p data-tool="mdnice编辑器"><code>whenIdle()</code> 也没有简单等待某个固定 Promise。它反复比较 <code>activityDone</code>：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs"><span class="hljs-keyword">do</span> {
  <span class="hljs-keyword">await</span> (activity = <span class="hljs-keyword">this</span>.activityDone)
} <span class="hljs-keyword">while</span> (activity !== <span class="hljs-keyword">this</span>.activityDone)
</code></pre>
<p data-tool="mdnice编辑器">等待期间如果 activity 被新的 driver 替换，调用方会继续等待。否则 <code>whenIdle()</code> 可能在旧 driver 结束、新 driver 已经启动的窗口里提前返回。</p>
<p data-tool="mdnice编辑器">在多插件环境中，idle 是一个可观察状态，也是一种同步承诺。这个承诺需要覆盖重启竞态，不能只看某次异步任务是否 resolve。</p>
<h1 data-tool="mdnice编辑器"><span class="content">Turn 状态机</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 在领取输入之前写入 <code>turn/start</code>。</p>
<p data-tool="mdnice编辑器">pre-step 可能拒绝输入，系统提示组装可能抛错，首步消息也可能被插件改写为空。无论发生哪种情况，日志中都存在一个完整 turn，并由唯一的 <code>turn/end</code> 收口。</p>
<p data-tool="mdnice编辑器">turn 内部维护两个变量：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>turnEnds</code>：当前已知的结束原因；</section>
</li>
<li>
<section><code>target</code>：本轮 pre-step 应领取 <code>next-turn</code> 还是 <code>next-step</code>。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">第一个 step 从 <code>next-turn</code> 开始。后续 step 转向 <code>next-step</code>。普通用户 prompt 因此拥有独立 turn，工具附加上下文、运行时 context 和 steering 则可以进入当前 turn 的下一 step。</p>
<p data-tool="mdnice编辑器">一次循环大致经历以下过程：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">创建 step 编号
→ 从 inbox 领取消息
→ 组装系统提示和运行时 context
→ 执行 agent/pre-step
→ 记录 step/start
→ 写入 user/message
→ 调用模型并执行工具
→ 记录 step/end
→ 判断是否继续
</code></pre>
<p data-tool="mdnice编辑器">如果 <code>agent/pre-step</code> 返回 <code>reject</code>，turn 以 <code>blocked</code> 结束，模型不会被调用。</p>
<p data-tool="mdnice编辑器">如果首个 step 的消息为空，turn 以 <code>completed</code> 结束，也不会消耗模型请求。这覆盖了一个边缘场景：wakeup 已经打开 turn，随后消息被移除，或者 pre-step 插件把消息改写为空。边界既然已经建立，日志仍需闭合。</p>
<p data-tool="mdnice编辑器">如果某个 step 已经产生结束结果，但下一次 pre-step 没有消息，循环退出。若仍有消息，就继续执行。</p>
<p data-tool="mdnice编辑器">这里需要留意 <code>turnEnds</code> 与 <code>stepEnd</code> 的关系。<code>step()</code> 可以返回：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>completed</code>；</section>
</li>
<li>
<section><code>max-tokens</code>；</section>
</li>
<li>
<section><code>null</code>。</section>
</li>
</ul>
<p data-tool="mdnice编辑器"><code>null</code> 表示工具执行完毕，模型还欠一次回复。它是继续循环的协议状态，和错误、未知结果没有关系。</p>
<p data-tool="mdnice编辑器">turn 的结束判断由「模型债务」与「消息债务」共同决定。模型债务来自 tool call，消息债务来自 inbox。只检查其中一项都会出错。</p>
<h1 data-tool="mdnice编辑器"><span class="content">停止钩子</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 在 turn 准备收口时触发 <code>agent/turn-stopping</code>。</p>
<p data-tool="mdnice编辑器">这个扩展点没有返回 <code>true</code> 或 <code>false</code>。插件如果希望 turn 继续，需要向 <code>next-step</code> 写入消息。hook 执行完成后，核心循环重新检查 inbox：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">turn 已经可以结束
→ next-step 为空
→ 触发 agent/turn-stopping
→ 再次检查 next-step
→ 为空则关闭
→ 非空则进入下一 step
</code></pre>
<p data-tool="mdnice编辑器">这是数据驱动的方式。</p>
<p data-tool="mdnice编辑器">多个插件都能监听 stopping。如果采用 <code>shouldContinue(): boolean</code>，组合规则会立刻变得棘手：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>任意插件返回 true 就继续？</section>
</li>
<li>
<section>所有插件都返回 true 才继续？</section>
</li>
<li>
<section>后执行的插件能否覆盖先执行的结果？</section>
</li>
<li>
<section>插件决定继续，却没有提供下一步输入，模型该看到什么？</section>
</li>
<li>
<section>插件决定停止，但另一个插件已经排入 steering，谁的优先级更高？</section>
</li>
</ul>
<p data-tool="mdnice编辑器">DeepSeek Harness 用 inbox 消除了大部分布尔值合并问题（这个设计和 Golang 的竞态处理逻辑类似，使用类似消息队列来处理竞态）。继续执行必须产生一条可消费的数据。最终判定依赖队列状态，而非插件调用顺序。</p>
<p data-tool="mdnice编辑器">当然，这也是需要代价的。</p>
<p data-tool="mdnice编辑器">插件作者需要理解 inbox target、turn 边界和 wakeup 语义。一个简单的「再跑一次」操作，需要构造 <code>UserMessage</code> 并写入正确队列。框架学习成本高于直接回调。</p>
<p data-tool="mdnice编辑器">这种成本在单体应用里可能显得繁琐。进入可恢复、多插件、可审计系统后，显式消息通常比隐式布尔控制更可靠。日志和调试工具能看到「为什么继续」，而不是只看到某个 hook 曾经返回 true。</p>
<h1 data-tool="mdnice编辑器"><span class="content">Step 债务</span></h1>
<p data-tool="mdnice编辑器"><code>step()</code> 最关键的职责，是判断模型是否还欠一次回复。</p>
<p data-tool="mdnice编辑器">模型流结束后，<code>BlockAssembler</code> 给出 finish 状态。如果 finish 是 error 或 aborted，系统先触发 <code>agent/request-error</code> waterfall。插件可以选择 retry。没有 retry 动作时，代码抛出结构化 <code>LlmError</code>。</p>
<p data-tool="mdnice编辑器">正常组装出 assistant message 后，判断顺序如下：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>finish 为 <code>max-tokens</code>，返回 <code>max-tokens</code>；</section>
</li>
<li>
<section>没有 tool call，返回 <code>completed</code>；</section>
</li>
<li>
<section>存在 tool call，执行整批工具；</section>
</li>
<li>
<section>工具批次声明 concluded，返回 <code>completed</code>；</section>
</li>
<li>
<section>普通工具批次返回 <code>null</code>。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">顺序不能随便改。</p>
<p data-tool="mdnice编辑器">最大 token 状态在工具解析之前返回，因此响应中即使出现 tool call，也不会被执行。截断输出可能包含不完整的 JSON 参数。某些流式解析器会尝试补全 JSON，补全后甚至能通过 schema 验证，但语义字段可能已经缺失。执行这类调用会产生真实副作用。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 的选择偏保守：整轮以 <code>max-tokens</code> 收口，等待外部策略决定是否继续。</p>
<p data-tool="mdnice编辑器">Pi 采用另一种处理。<code>stopReason === "length"</code> 且消息包含工具调用时，它会给所有工具生成错误结果，说明参数可能被截断，没有实际执行，然后把这些结果交回模型。模型有机会在下一 turn 重发完整调用。</p>
<p data-tool="mdnice编辑器">两者的恢复能力不同。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 保留了清晰的 durable turn reason，运维侧能准确识别 token ceiling。自动继续需要 Goal Driver、用户输入或其他插件参与。Pi 更倾向在当前 invocation 内自我修复，模型可能马上重发工具调用。</p>
<p data-tool="mdnice编辑器">我在具有写操作的 Agent 中会选择 DeepSeek Harness 的策略。达到输出上限本身意味着模型状态不完整，自动延续可能重复前一批操作。只读研究型 Agent 可以采用 Pi 的方式，失败工具结果能减少一次人工干预。</p>
<h1 data-tool="mdnice编辑器"><span class="content">粘性结果</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 的 <code>max-tokens</code> 具有粘性。</p>
<p data-tool="mdnice编辑器">某个 step 达到上限后，插件仍可能在 <code>turn-stopping</code> 阶段补入 steering，让 turn 再执行几个 step。后续 step 即使正常完成，最终 <code>turn/end</code> 仍然保留 <code>max-tokens</code>。</p>
<p data-tool="mdnice编辑器">判断逻辑是：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs"><span class="hljs-keyword">if</span> (
  turnEnds === <span class="hljs-literal">null</span> ||
  turnEnds.kind !== <span class="hljs-string">'max-tokens'</span>
)
  turnEnds = stepEnd
</code></pre>
<p data-tool="mdnice编辑器">这里记录的是整个 turn 的质量，而非最后一次 step 的结果。</p>
<p data-tool="mdnice编辑器">如果不做粘性处理，可能产生如下日志：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">step 1: max-tokens
step 2: 插件要求继续
step 3: completed
turn/end: completed
</code></pre>
<p data-tool="mdnice编辑器">消费端会丢失曾经发生的截断。监控系统无法统计 token ceiling，Goal Driver 也可能把这轮当成完整成功并继续自动调度。</p>
<p data-tool="mdnice编辑器">把 turn reason 视为聚合结果后，合并规则就需要专门设计。DeepSeek Harness 当前只对 <code>max-tokens</code> 做了优先保留。错误和取消会直接离开主流程，不参与后续合并。</p>
<p data-tool="mdnice编辑器">如果未来加入诸如 <code>partial</code>、<code>policy-warning</code>、<code>budget-exhausted</code> 等状态，简单覆盖会越来越脆弱。更稳妥的方向是为 reason 建立显式优先级或累积 facts，再由投影层生成最终 reason。当前 union 规模尚小，现有实现足够清楚。</p>
<h1 data-tool="mdnice编辑器"><span class="content">工具收口</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 允许工具通过结果上的 <code>concludesTurn</code> 宣布当前 turn 可以收口。</p>
<p data-tool="mdnice编辑器">工具批次采用 OR 聚合：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">任意一个已提交结果 concludesTurn === true
→ 整批 concluded === true
</code></pre>
<p data-tool="mdnice编辑器">它表达的是「模型默认不再欠一次工具结果回访」。</p>
<p data-tool="mdnice编辑器"><code>concludesTurn</code> 没有直接打断同批工具，也没有清空 <code>next-step</code>：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>已启动的并行工具继续运行；</section>
</li>
<li>
<section>结果按模型调用顺序提交；</section>
</li>
<li>
<section><code>additionalContexts</code> 写入 <code>next-step</code>；</section>
</li>
<li>
<section>同期到达的 steering 也写入 <code>next-step</code>；</section>
</li>
<li>
<section>只要 <code>next-step</code> 非空，turn 仍会进入下一 step。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">因此，工具结束能力属于软收口。它取消了自动回访债务，无法压过已经存在的消息债务。</p>
<p data-tool="mdnice编辑器">这套语义适合权威型工具。例如某个交互工具已经取得用户最终决定，或者某个提交工具已经完成不可逆事务，模型没有必要再生成一段中间解释。工具可以建议收口，同时保留系统上下文和用户 steering 的优先级。</p>
<p data-tool="mdnice编辑器">Pi 的工具终止采用 AND 聚合：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs"><span class="hljs-keyword">return</span> finalizedCalls.length &gt; <span class="hljs-number">0</span> &amp;&amp;
  finalizedCalls.every(
    <span class="hljs-function"><span class="hljs-params">finalized</span> =&gt;</span> finalized.result.terminate === <span class="hljs-literal">true</span>
  )
</code></pre>
<p data-tool="mdnice编辑器">只有非空批次中的每个 finalized result 都带有 <code>terminate: true</code>，整批才终止。</p>
<p data-tool="mdnice编辑器">并行批次中，一个工具不应单方面吞掉其他工具结果。Pi 要求整批达成一致，语义更保守。DeepSeek Harness 允许一个权威工具宣布收口，扩展能力更强，也更依赖工具契约。</p>
<p data-tool="mdnice编辑器">选择 OR 还是 AND，取决于 <code>terminate</code> 的业务含义。</p>
<p data-tool="mdnice编辑器">如果它代表「任一工具发现全局终止条件」，OR 合理，比如用户取消、权限拒绝、事务已完成。</p>
<p data-tool="mdnice编辑器">如果它代表「当前工具不需要模型处理自己的结果」，AND 更稳妥，因为其他工具可能仍需模型解释。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 通过 <code>next-step</code> 上下文缓和了 OR 的侵略性，但无法覆盖所有情况。假设两个并行工具都成功，一个写数据库并 concludes，另一个返回一份需要模型分析的报告，又没有追加 context。模型回访会被跳过。工具注册规范需要限制哪些工具可以 conclude，不能把它当成普通便利字段。</p>
<h1 data-tool="mdnice编辑器"><span class="content">顺序提交</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 的工具调度器还有一项容易被忽略的约束：执行可以并行，提交保持模型顺序。</p>
<p data-tool="mdnice编辑器">调度器维护：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>nextToStart</code>；</section>
</li>
<li>
<section><code>started</code>；</section>
</li>
<li>
<section><code>committed</code>；</section>
</li>
<li>
<section><code>inFlight</code>；</section>
</li>
<li>
<section>按调用位置排列的 <code>slots</code>。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">并行工具完成时，只把结果放入对应 slot。<code>commitReady()</code> 从 <code>committed</code> 开始，连续提交已经就绪的结果。后面的工具先完成，也要等待前面的 slot。</p>
<p data-tool="mdnice编辑器">这会增加尾部等待时间，但能保证：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>tool/result</code> 顺序稳定；</section>
</li>
<li>
<section>additional context 顺序稳定；</section>
</li>
<li>
<section>replay 结果稳定；</section>
</li>
<li>
<section>插件观察顺序稳定；</section>
</li>
<li>
<section>同一模型响应在多次运行中的日志结构更一致。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">模型生成了 A、B、C 三个调用。若 C 最先完成，直接提交 C 会让后续上下文受网络时序影响。对于事件溯源系统，这类非确定性会扩大恢复和测试成本。</p>
<p data-tool="mdnice编辑器">Pi 的并行执行也使用 <code>Promise.all</code> 保留输入数组顺序，最终 tool result message 按原调用顺序发出。两套系统都接受并行执行带来的延迟收益，同时拒绝完成时序污染模型上下文。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 额外支持动态重分类。并行池补充新任务前，会重新读取工具当前 execution mode。前一个工具可能修改注册表，使后续工具从 parallel 变成 exclusive。调度器会停止补充，等待当前池排空，再把 exclusive call 留给下一个 barrier。</p>
<p data-tool="mdnice编辑器">这种灵活性会增加 scheduler 状态数量。适合「工具本身能改变工具环境」的 Harness。工具目录完全静态时，预先分组会更简单。</p>
<h1 data-tool="mdnice编辑器"><span class="content">取消语义</span></h1>
<p data-tool="mdnice编辑器">工具执行期间的取消，比模型流取消复杂得多。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 的原则可以概括成三条：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>停止补充尚未启动的调用；</section>
</li>
<li>
<section>清空已经启动的调用；</section>
</li>
<li>
<section>为因取消而跳过的工具写入合成错误结果。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">已启动工具可能已经产生副作用，框架不能假装它没有运行。调度器等待这些调用 settle，并按模型顺序提交结果和 additional contexts。</p>
<p data-tool="mdnice编辑器">未启动工具则写入一对完整事件：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">tool/call
tool/result(error: aborted before dispatch)
</code></pre>
<p data-tool="mdnice编辑器">这样可以维持模型工具协议的配对关系。恢复和 replay 时，每个模型产生的 tool call 都有对应结果。</p>
<p data-tool="mdnice编辑器">内部 scheduler failure 的处理更克制。它停止新 dispatch，等待已经启动的调用结束，然后抛出第一个失败。它不会为剩余调用伪造结果。因为 scheduler failure 与用户取消不同，系统无法确认未提交调用处于什么语义状态。伪造恢复结果可能掩盖调度器缺陷。</p>
<p data-tool="mdnice编辑器">这里体现出事件日志的核心要求：日志不只服务 UI，它还是恢复协议。任何 synthetic event 都需要有确定语义。为了让数组长度好看而补事件，会污染后续决策。</p>
<p data-tool="mdnice编辑器">Pi 的实现更偏运行时事件流。工具执行异常会被转换成 error tool result，随后继续进入上下文。取消时顺序模式会在每次工具后检查 signal，平行模式也会停止准备后续调用。它的核心目标是把本次 invocation 的 message stream 完整交给调用方，持久日志的闭合约束没有 DeepSeek Harness 那么强。</p>
<h1 data-tool="mdnice编辑器"><span class="content">原因体系</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 的 <code>TurnEndReason</code> 包含六种核心结果：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>completed</code>；</section>
</li>
<li>
<section><code>blocked</code>；</section>
</li>
<li>
<section><code>max-tokens</code>；</section>
</li>
<li>
<section><code>aborted</code>；</section>
</li>
<li>
<section><code>error</code>；</section>
</li>
<li>
<section><code>interrupted</code>。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这些状态回答的是整个 turn 如何结束。</p>
<p data-tool="mdnice编辑器"><code>completed</code> 覆盖正常文本停止、工具要求收口，以及首步为空。它不承诺长期 Goal 完成。</p>
<p data-tool="mdnice编辑器"><code>blocked</code> 来自 pre-step reject。输入可能已经被 inbox 领取，但策略层拒绝进入模型调用。driver 当前会停止，其他尚未领取的普通 prompt 可以继续留在队列。</p>
<p data-tool="mdnice编辑器"><code>max-tokens</code> 表示至少一个 step 达到输出上限。它保留为 turn 级质量信号。</p>
<p data-tool="mdnice编辑器"><code>aborted</code> 携带取消原因，包括 user、parent、hook 和 disposed。持久导入还可能出现 legacy cause。</p>
<p data-tool="mdnice编辑器"><code>error</code> 保存结构化 <code>LlmFailure</code>。<code>LlmError</code> 的信息原样保留，其他异常会被压平为 <code>errorChain</code> 文本，并使用 <code>UNKNOWN</code> code。</p>
<p data-tool="mdnice编辑器"><code>interrupted</code> 由持久化恢复层产生。进程崩溃后，后端发现一个开放 turn，会用该状态闭合。live loop 不会主动写入它。</p>
<p data-tool="mdnice编辑器">把 crash repair 与 runtime abort 分开很有必要。abort 表示程序收到了取消请求，并有机会执行清理；interrupted 表示生命周期突然消失。两者对副作用审计、重试决策和用户提示都有不同含义。</p>
<p data-tool="mdnice编辑器">Pi 主要保留 assistant message 的 <code>stopReason</code>：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>stop</code>；</section>
</li>
<li>
<section><code>length</code>；</section>
</li>
<li>
<section><code>toolUse</code>；</section>
</li>
<li>
<section><code>error</code>；</section>
</li>
<li>
<section><code>aborted</code>。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">它更接近最后一次模型调用的结束事实。DeepSeek Harness 的 reason 聚合了多 step turn 的结果。一个字段偏 provider，一个字段偏业务生命周期。</p>
<p data-tool="mdnice编辑器">从监控角度看，Pi 的 stopReason 更利于分析模型行为；DeepSeek Harness 的 turn reason 更利于分析任务执行。生产系统通常两类数据都需要。DeepSeek Harness 通过 <code>assistant/message</code>、request events 和 <code>turn/end</code> 同时保留两层事实，日志体积也会更大。</p>
<h1 data-tool="mdnice编辑器"><span class="content">Goal 外循环</span></h1>
<p data-tool="mdnice编辑器">长期 Goal 无法靠核心 turn 的 <code>completed</code> 判断。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 采用外围 Round Driver。它监听 Agent 进入 idle，然后检查：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>Agent 实例是否仍然有效；</section>
</li>
<li>
<section>inbox 是否存在竞争中的普通 prompt；</section>
</li>
<li>
<section>Goal 是否存在；</section>
</li>
<li>
<section>Goal phase 是否为 active；</section>
</li>
<li>
<section>当前 activation 是否 armed；</section>
</li>
<li>
<section>已启动 round 数是否低于上限。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">满足条件后，Driver 构造一条来源为 goal 的 user message，并调用 <code>agent.followup()</code>。新消息进入 <code>next-turn</code>，打开全新的 turn。</p>
<p data-tool="mdnice编辑器">时序如下：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">turn/end(completed)
→ agent/status: idle
→ Goal Driver 读取持久状态
→ flush
→ followup(goal round)
→ turn/start
</code></pre>
<p data-tool="mdnice编辑器">这种结构给每个 Goal Round 一个完整、独立、可恢复的生命周期。进程在两个 Round 之间退出，恢复时可以从最后一个闭合 turn 继续判断。一个覆盖几十轮的巨大 while 会让持久化边界、用户抢占和异常恢复都更困难。</p>
<p data-tool="mdnice编辑器">Goal 还维护 phase 与 activation 两组状态。</p>
<p data-tool="mdnice编辑器">持久 phase 描述业务状态：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>active；</section>
</li>
<li>
<section>complete；</section>
</li>
<li>
<section>blocked；</section>
</li>
<li>
<section>paused。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">进程内 activation 描述自动续行授权：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>armed；</section>
</li>
<li>
<section>disarmed。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">把两者拆开可以处理 resume 和 fork。一个持久 Goal 仍然 active，不代表新进程应该自动继续消耗预算。恢复后的 active Goal 默认 disarmed，需要人类再次授权。否则某次服务重启可能让多个历史任务同时复活。</p>
<p data-tool="mdnice编辑器">达到最大 Round 数时，Goal 会进入 blocked，并记录 round-limit。<code>max-tokens</code>、Agent error、flush failure、driver failure 会 disarm。取消路径也会 pause 或 disarm，防止 idle 后被 Goal Driver 再次拉起。</p>
<p data-tool="mdnice编辑器">这里采用了 fail-closed 策略。长期自治任务遇到状态不确定时停止续行。对成本敏感、有写权限的 Agent，这是合理默认值。自动重试可以放在更高层，由持久预算和幂等策略约束。</p>
<h1 data-tool="mdnice编辑器"><span class="content">Goal 收尾</span></h1>
<p data-tool="mdnice编辑器">Goal 状态改为 complete 或 blocked 后，当前 turn 通常还会再调用一次模型。</p>
<p data-tool="mdnice编辑器"><code>update_goal</code> 工具会修改持久状态，并通过 deferred context 注入收尾指令。工具结果与 <code>&lt;goal_complete&gt;</code> 或 <code>&lt;goal_blocked&gt;</code> context 进入下一 step，模型生成最终面向用户的说明。之后 turn 以 completed 结束。Agent 进入 idle，Goal Driver 读取到非 active phase，不再唤醒。</p>
<p data-tool="mdnice编辑器">这解决了状态完成与用户可见输出之间的时间差。</p>
<p data-tool="mdnice编辑器">如果 <code>update_goal(complete)</code> 直接 conclude turn，持久状态已经完成，用户界面可能只看到工具卡片，没有自然语言总结。若让模型先写总结再更新状态，模型或进程可能在总结期间失败，Goal 又会保持 active。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 先提交状态，再执行展示收尾。恢复时即便缺少最终文本，Goal 也不会重复工作。用户体验层可以根据 complete 状态补一条提示，或者提供重新生成总结的操作。</p>
<p data-tool="mdnice编辑器">代价是 complete 后多一次模型调用，也会增加少量 token 和延迟。我认为这笔成本合理。长期任务的最终输出属于交付结果，不能依赖 UI 猜测工具状态。</p>
<h1 data-tool="mdnice编辑器"><span class="content">Pi 双循环</span></h1>
<p data-tool="mdnice编辑器">Pi 的 <code>runLoop()</code> 使用内外两层循环。</p>
<p data-tool="mdnice编辑器">内层条件为：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">hasMoreToolCalls || pendingMessages.length &gt; 0
</code></pre>
<p data-tool="mdnice编辑器">它处理工具回访和 steering。外层负责 Agent 本来要停止时的 follow-up。</p>
<p data-tool="mdnice编辑器">一次内层迭代包括：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">prepareNextTurn
→ 处理 pending steering
→ 调用模型
→ 执行工具
→ 发出 turn_end
→ shouldStopAfterTurn
→ 拉取新的 steering
</code></pre>
<p data-tool="mdnice编辑器">内层耗尽后，系统调用 <code>getFollowUpMessages()</code>。存在 follow-up 时，将它们设置为 pending，重新进入内层。没有 follow-up 时发出 <code>agent_end</code>。</p>
<p data-tool="mdnice编辑器">Pi 提供三个高杠杆控制点：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>getSteeringMessages()</code>；</section>
</li>
<li>
<section><code>getFollowUpMessages()</code>；</section>
</li>
<li>
<section><code>shouldStopAfterTurn()</code>。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这套接口很适合嵌入应用。</p>
<p data-tool="mdnice编辑器">steering 处理当前 activity 内的即时干预；follow-up 处理 Agent 自然收口后的补充工作；shouldStopAfterTurn 提供宿主级截停。扩展作者不需要理解持久 inbox，也不需要创建新的状态投影。</p>
<p data-tool="mdnice编辑器">复杂度被交给宿主。多个扩展共同提供 follow-up 时，需要上层决定合并方式。进程恢复后，回调内部的临时状态如何重建，也由应用实现。若不同扩展分别定义 Goal、Plan 和审批流程，结束语义会逐渐碎片化。</p>
<p data-tool="mdnice编辑器">Pi core 的目标是提供一个小而可组合的运行时。它没有承诺统一的长期任务协议。这个边界和 DeepSeek Harness 的产品定位并不相同。</p>
<h1 data-tool="mdnice编辑器"><span class="content">失败路径</span></h1>
<p data-tool="mdnice编辑器">Pi 遇到 assistant <code>error</code> 或 <code>aborted</code> 时，会发出 <code>turn_end</code> 和 <code>agent_end</code>，然后结束当前 invocation。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 会先区分流 finish error、显式 abort 和普通异常。</p>
<p data-tool="mdnice编辑器">请求错误可以进入 <code>agent/request-error</code> waterfall。插件能够依据 provider、failure、retry policy 和 signal 决定 retry。重试发生在同一个 step 内，重新创建 assembler，再发一次请求。外部 turn 和 step 边界保持不变。</p>
<p data-tool="mdnice编辑器">这种设计使日志更紧凑，但需要谨慎处理重复请求。前一次失败若已经在 provider 侧产生计费，session 中可能只有请求重试后的最终 assistant message。底层观测系统需要独立记录 attempt，不能只靠 conversation event log 统计模型调用次数。</p>
<p data-tool="mdnice编辑器">Pi 通常由 stream implementation 或上层配置承担 retry。Agent Loop 接收到的 final message 已经带有 stopReason。职责划分更轻，调用方也更容易替换模型层。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 将 request header、adapter defaults、request context 和 conversation surface 都纳入 session 语义，retry 与恢复自然更靠近 loop。架构更重，换来统一的请求重建能力。</p>
<h1 data-tool="mdnice编辑器"><span class="content">请求序列</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 每次构建请求时都会比较当前 header 与 session 中的 baseline。</p>
<p data-tool="mdnice编辑器">header 包含：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>provider 和 model；</section>
</li>
<li>
<section>reasoning effort、max tokens 等调用配置；</section>
</li>
<li>
<section>adapter materialized defaults；</section>
</li>
<li>
<section>system prompt；</section>
</li>
<li>
<section>tool schemas。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">首个 header 记录为 initial 或 resume。配置变化时记录 change。显式开始新消息序列或 surface replacement 后，会记录 series。</p>
<p data-tool="mdnice编辑器">结束判断和 request series 没有直接控制关系，但它影响「同一个上下文连续体」的解释。Goal 的自动 Round 可以标记 <code>startsRequestSeries: true</code>，让每轮长期任务拥有独立请求序列。缓存、展示和重放层因此能识别边界。</p>
<p data-tool="mdnice编辑器">如果只依靠 turn start 判断新序列，会把工具回访、用户 follow-up、Goal Round 和 compaction 后续请求全部归入一种语义。DeepSeek Harness 单独记录 series，是在为后续缓存和重建保留信息。</p>
<p data-tool="mdnice编辑器">成本是日志事件更多，header equality 与 canonicalization 也必须稳定。工具 schema 排序、空字段处理或 adapter defaults 若不规范，可能制造大量无意义 change 事件。代码使用 canonical header 和 deep freeze，正是在控制这种漂移。</p>
<h1 data-tool="mdnice编辑器"><span class="content">日志消费</span></h1>
<p data-tool="mdnice编辑器">消费 DeepSeek Harness 日志时，先问清业务方想判断哪一层结束。</p>
<p data-tool="mdnice编辑器">判断一次模型调用完成，应读取 step 内的 assistant message、stream finish 和 usage。</p>
<p data-tool="mdnice编辑器">判断 turn 是否闭合，应寻找配对的 <code>turn/start</code> 与 <code>turn/end</code>。只有 assistant message 不够。pre-step reject、空 turn、异常和 crash repair 都可能产生不同结构。</p>
<p data-tool="mdnice编辑器">判断当前 Agent 是否暂无活动，应读取 status 或等待 <code>whenIdle()</code>。最后一条 turn/end 仍不足以证明 idle，因为 driver 可能已经打开下一 turn。</p>
<p data-tool="mdnice编辑器">判断长期 Goal 是否结束，应读取 Goal phase。complete、blocked 和 paused 都会停止自动 Round，业务含义各不相同。</p>
<p data-tool="mdnice编辑器">判断 Plan 是否退出，应读取最新 <code>plan/mode.active</code>。看到 <code>exit_plan_mode</code> 调用，只能说明模型提出了退出请求。审批可能被拒绝，pending intent 也可能尚未在 pre-step 提交。</p>
<p data-tool="mdnice编辑器">判断会话以后是否还会运行，需要观察 Agent 是否 disposed、是否从 registry 移除，以及外围 scheduler 是否仍能发送消息。idle 和 completed 都没有永久性。</p>
<p data-tool="mdnice编辑器">Pi 的消费方式更简单：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>assistant <code>stopReason</code> 描述模型调用；</section>
</li>
<li>
<section><code>turn_end</code> 描述一次模型与工具批次；</section>
</li>
<li>
<section><code>agent_end</code> 描述当前 invocation；</section>
</li>
<li>
<section>Goal、Plan 和工作流完成度读取具体扩展状态。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">即使已经收到 <code>agent_end</code>，宿主仍然可以再次调用 Agent Loop。它同样不具备永久终止含义。</p>
<h1 data-tool="mdnice编辑器"><span class="content">工程取舍</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 为结束判断支付了较高复杂度。</p>
<p data-tool="mdnice编辑器">它需要：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>phase 状态机；</section>
</li>
<li>
<section>inbox 分类；</section>
</li>
<li>
<section>wake latch；</section>
</li>
<li>
<section>平衡的 turn 和 step 事件；</section>
</li>
<li>
<section>surface projection；</section>
</li>
<li>
<section>request header 重建；</section>
</li>
<li>
<section>crash repair；</section>
</li>
<li>
<section>Goal activation；</section>
</li>
<li>
<section>Plan boundary intent；</section>
</li>
<li>
<section>有序工具提交。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这些机制会增加代码量、测试矩阵和插件开发门槛。一个简单聊天产品采用全套设计，投入可能超过收益。</p>
<p data-tool="mdnice编辑器">它适合以下场景：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>会话需要跨进程恢复；</section>
</li>
<li>
<section>Agent 拥有高风险工具；</section>
</li>
<li>
<section>多个插件会同时 steering；</section>
</li>
<li>
<section>需要长期 Goal 与预算控制；</section>
</li>
<li>
<section>日志需要审计和 replay；</section>
</li>
<li>
<section>Plan、审批和执行必须跨边界保持一致；</section>
</li>
<li>
<section>子 Agent 和父 Agent 存在取消传播。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">Pi 的循环更适合：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>单次 invocation 生命周期清晰；</section>
</li>
<li>
<section>宿主已经拥有自己的持久化层；</section>
</li>
<li>
<section>扩展数量有限；</section>
</li>
<li>
<section>工作流由应用统一控制；</section>
</li>
<li>
<section>希望快速接入多模型与工具；</section>
</li>
<li>
<section>对 session event vocabulary 没有强约束。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">Pi 的小核心减少了框架内部状态。业务一旦开始增加 Goal、审批、自动 follow-up、并发工具策略、恢复和预算，宿主会逐步补齐类似机制。复杂度没有消失，只是落在不同层。</p>
<p data-tool="mdnice编辑器">团队选择框架时，不应只比较 Agent Loop 文件有多少行。要看系统准备把「继续运行的权力」放在哪里。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 把权力分散给模型 finish、工具结果、inbox、生命周期 hook 和外围持久状态机，并通过事件日志协调。Pi 把权力集中在当前循环和几个宿主回调中，扩展层可以自行定义更高层协议。</p>
<h1 data-tool="mdnice编辑器"><span class="content">设计建议</span></h1>
<p data-tool="mdnice编辑器">如果我们团队要自行实现 Agent Loop，我会保留以下原则。</p>
<p data-tool="mdnice编辑器">第一，区分模型结束、工具批次结束、用户 turn 结束和长期任务结束。类型和事件名也要分开，避免一个 <code>done</code> 字段横跨所有层级。</p>
<p data-tool="mdnice编辑器">第二，继续执行需要携带数据。插件要求再跑一轮时，应提交 steering 或 follow-up message。单独返回布尔值会让调试和组合越来越困难。</p>
<p data-tool="mdnice编辑器">第三，工具调用后的模型回访应被建模为债务。普通工具结果产生一次回复债务，conclude 或 terminate 可以消除债务，steering 又会重新增加消息债务。用这套视角设计状态机，比堆叠 <code>if (hasToolCalls)</code> 更稳定。</p>
<p data-tool="mdnice编辑器">第四，最大 token 需要独立结果，且不要执行疑似截断的工具参数。自动修复可以存在，但必须显式记录恢复过程。</p>
<p data-tool="mdnice编辑器">第五，并行工具允许乱序完成，持久结果应按模型顺序提交。除非协议明确支持乱序 tool result，否则不要让网络时序改变上下文。</p>
<p data-tool="mdnice编辑器">第六，取消必须区分已启动与未启动调用。已启动工具要排空或记录未知结果，未启动工具需要完整的 synthetic result，具体选择取决于日志协议。</p>
<p data-tool="mdnice编辑器">第七，idle 不能充当长期完成信号。Goal phase、workflow state 和 Agent lifecycle 应拥有独立状态。</p>
<p data-tool="mdnice编辑器">第八，模式切换要提交在请求边界。Plan、权限、模型路由和工具目录的变化，都不应在一个已开始的请求批次中途生效。</p>
<p data-tool="mdnice编辑器">第九，恢复后的自动续行默认关闭。长期 Agent 会消耗资金，也可能修改外部系统。新进程接管旧任务时，应重新取得授权或租约。</p>
<p data-tool="mdnice编辑器">第十，结束原因需要兼顾机器消费。<code>completed</code>、<code>blocked</code>、<code>max-tokens</code>、<code>aborted</code>、<code>error</code>、<code>interrupted</code> 这类结构化 union，比一段自然语言错误更适合监控、重试和审计。</p>
<h1 data-tool="mdnice编辑器"><span class="content">架构落点</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 把结束判断分布在四个控制面上。</p>
<p data-tool="mdnice编辑器">模型输出控制 step：还有没有工具回访债务。</p>
<p data-tool="mdnice编辑器">inbox 控制 turn：还有没有当前轮待处理消息。</p>
<p data-tool="mdnice编辑器">driver 控制 activity：队列耗尽后是否进入 idle。</p>
<p data-tool="mdnice编辑器">Goal 与 Plan 控制工作流：idle 后是否开启新 Round，下一请求采用哪种策略。</p>
<p data-tool="mdnice编辑器">Pi 将更多判断放在同一个运行循环里。工具调用和 pending steering 驱动内层循环，follow-up 驱动外层循环，<code>shouldStopAfterTurn</code> 提供宿主截停点，队列耗尽后发出 <code>agent_end</code>。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 更接近一个事件溯源的 Agent Runtime。Pi 更接近一个可嵌入的 Agent 执行内核。</p>
<p data-tool="mdnice编辑器">评价两者没啥意义，我们需要关注的是其系统边界。</p>
<p data-tool="mdnice编辑器">如果产品只需要一次 prompt 驱动的工具循环，Pi 的结构更容易维护。若产品已经出现跨轮 Goal、审批、恢复、多插件 steering 和高风险工具，DeepSeek Harness 的分层会减少后期协议冲突。</p>
<p data-tool="mdnice编辑器">Agent Loop 最难处理的部分，从来都不是让模型继续调用工具。真正消耗工程时间的是：它为什么继续、谁允许它继续、失败后还能不能继续、恢复后应不应该继续，以及日志能否解释它曾经做过的每一次选择。</p>
<p data-tool="mdnice编辑器">以上。</p>
</section>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/08/deepseek-harness-and-pi-agent-loop-end/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>DeepSeek Harness 的 Cordis 插件架构</title>
		<link>https://www.phppan.com/2026/08/deepseek-harness-cordis/</link>
		<comments>https://www.phppan.com/2026/08/deepseek-harness-cordis/#comments</comments>
		<pubDate>Sun, 23 Aug 2026 03:36:05 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[Agent Harness]]></category>
		<category><![CDATA[AIAgent架构]]></category>
		<category><![CDATA[DeepSeek]]></category>
		<category><![CDATA[DeepSeekHarness]]></category>
		<category><![CDATA[harness]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2531</guid>
		<description><![CDATA[DeepSeek Harness 把模型、工具、会话、沙箱、文件系统、Agent 循环、调度和 UI 都交给插 [&#8230;]]]></description>
				<content:encoded><![CDATA[<section id="nice" style="color: #000000;" data-tool="mdnice编辑器" data-website="https://www.mdnice.com">
<p data-tool="mdnice编辑器">DeepSeek Harness 把模型、工具、会话、沙箱、文件系统、Agent 循环、调度和 UI 都交给插件。</p>
<p data-tool="mdnice编辑器">系统没有一块承载业务能力的固定内核，Cordis 只维护上下文、服务注册、事件分发、依赖激活和副作用回收；Agent 能做什么，由启动时挂入的一棵插件树决定。</p>
<p data-tool="mdnice编辑器">传统的插件有注册容易，撤销困难；初始化容易，失败回滚困难；全局单例容易，局部覆盖困难等问题，而 Cordis 把这些困难收进了运行时语义，Harness 再用 session、agent 和 preset 的业务约束补齐它。</p>
<p data-tool="mdnice编辑器">今天聊了一下 DeepSeek Harness 的核心 Cordis 架构以及 Cordis 所谓「时空可组合」，究竟怎样落到代码里，又为何适合 Agent Harness 这类持续变化的运行时。</p>
<h1 data-tool="mdnice编辑器"><span class="content">插件的历史</span></h1>
<p data-tool="mdnice编辑器">插件架构的历史并不短。Eclipse 在二十多年前已经把 IDE 拆成插件、扩展点和扩展，宿主声明可扩展位置，其他插件通过清单贡献菜单、编辑器和处理逻辑。</p>
<p data-tool="mdnice编辑器">OSGi 更进一步，给 bundle 配置安装、启动、停止、更新和卸载生命周期，再通过共享服务注册表完成发布、查找和绑定。服务离开注册表时，依赖方必须跟着处理动态变化。</p>
<p data-tool="mdnice编辑器">今天看 Cordis，能找到这些设计的清晰痕迹：动态服务、生命周期、注册表、声明式依赖、事件通知都已有成熟先例。</p>
<p data-tool="mdnice编辑器">Cordis 没有发明插件系统。</p>
<p data-tool="mdnice编辑器">它简化并重新组合了这些概念，让它们适合 TypeScript 应用内的细粒度组装。OSGi 的部署单元是 bundle，模块、生命周期、服务和安全各有一层；Cordis 的执行单元是 Fiber，一个函数或一个 <code style="color: #ef7060;">Service</code> 子类就能成为插件。Eclipse 的扩展点通常由宿主定义结构化清单，Cordis 让服务和类型化事件直接成为扩展面。Agent 运行时里的工具、提示词片段、模型适配器和审批策略变化频繁，如果每次扩展都要引入重量级模块边界，团队很快会绕过框架，重新写回几个全局数组。</p>
<p data-tool="mdnice编辑器">Cordis 官方仓库把自己定位为「Meta-Framework of Spatiotemporal Composability」。</p>
<p data-tool="mdnice编辑器">从源码看，「元框架」表示它不规定 Agent、Web 服务或机器人该有什么组件，只提供构造框架所需的基础语义。DeepSeek Harness 在其上定义 <code style="color: #ef7060;">sessions</code>、<code style="color: #ef7060;">tools</code>、<code style="color: #ef7060;">llm</code>、<code style="color: #ef7060;">agents</code> 等服务，又用这些服务组装产品。Cordis 与 Harness 的关系更接近运行时机制和领域框架，而非通用插件平台与插件集合。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 还把 Cordis 源码直接放进 <code style="color: #ef7060;">vendor/</code>，固定在明确的上游提交，并将包名重映射到 <code style="color: #ef7060;">@deepseek-ai</code> 命名空间。</p>
<p data-tool="mdnice编辑器">项目维护的本地修改包含 Fiber 重入卸载加固、配置更新事务、HMR 精确监听、延迟配置解析等。这增加了维护成本，也换来了框架层的可审计性。Agent 能执行 Shell、修改文件、访问网络，插件生命周期出错会留下进程、监听器、终端模式或权限状态。</p>
<p data-tool="mdnice编辑器">此时依赖一个本地的代码白盒，会让人放心一些。</p>
<h1 data-tool="mdnice编辑器"><span class="content">调用链路图</span></h1>
<p data-tool="mdnice编辑器">简单的调用图如下所示：</p>
<figure data-tool="mdnice编辑器"><img src="https://files.mdnice.com/user/36365/c8ef3798-50e3-40a7-810e-c68c5f71ca03.png" alt="" /></figure>
<h1 data-tool="mdnice编辑器"><span class="content">最小内核</span></h1>
<p data-tool="mdnice编辑器">Cordis 的根对象是 <code style="color: #ef7060;">Context</code>。它同时承担依赖容器、插件挂载入口和事件入口。</p>
<p data-tool="mdnice编辑器">服务通过稳定名称出现在 <code style="color: #ef7060;">ctx</code> 上，例如 <code style="color: #ef7060;">ctx.tools</code>、<code style="color: #ef7060;">ctx.llm</code>、<code style="color: #ef7060;">ctx.sessions</code>。插件声明 <code style="color: #ef7060;">inject</code> 后，Cordis 只有在所需服务可用时才激活它；服务消失，依赖插件也会进入卸载或等待状态。配置文件中条目的先后顺序因此不承担启动顺序，依赖关系才承担。</p>
<p data-tool="mdnice编辑器">这种处理解决了传统插件系统的第一个顽疾：隐含初始化顺序。</p>
<p data-tool="mdnice编辑器">常见实现会遍历插件数组，依次调用 <code style="color: #ef7060;">init</code>。当工具依赖文件系统、文件系统依赖沙箱、UI 又依赖会话时，数组顺序就变成一套没有类型、没有诊断的依赖图。后来插入一个插件，顺序约束可能跨越几十个文件。</p>
<p data-tool="mdnice编辑器">Cordis 把需求写进 <code style="color: #ef7060;">inject</code>，Fiber 会为每项依赖保存当前实现；缺少依赖时保持 <code style="color: #ef7060;">PENDING</code>，依赖齐备后进入 <code style="color: #ef7060;">LOADING</code> 和 <code style="color: #ef7060;">ACTIVE</code>。启动失败则进入 <code style="color: #ef7060;">FAILED</code>。状态机至少让故障有了准确位置。</p>
<p data-tool="mdnice编辑器"><code style="color: #ef7060;">Context</code> 还是一个代理对象。</p>
<p data-tool="mdnice编辑器">插件直接读取 <code style="color: #ef7060;">ctx.tools</code> 时，反射层会检查它是否声明过依赖，并沿 Fiber 父链解析实现。未声明就读取会抛错；声明了但当前上下文不可用，也会抛出另一类错误。这种约束防止依赖藏在任意函数深处。源码里依然提供 <code style="color: #ef7060;">ctx.get(name)</code> 读取可选服务，区别在于调用方显式接受服务可能不存在。</p>
<p data-tool="mdnice编辑器">这里的「服务定义、服务提供方、消费方」三种角色被完整建模。比如文件系统能力不能只写一个接口，也不能只挂一个本地实现。定义方稳定调用协议，提供方接入本地目录或远程沙箱，消费方把能力变成模型可见工具。Harness 把这组关系称为 capability seam。替换沙箱时，Shell、PTY、LSP 只要依赖同一能力面，就能整体迁移到新的执行环境，消费方无需知道提供方运行在本机还是远端。</p>
<p data-tool="mdnice编辑器">「一切皆插件」有一个前提是：一切产品能力皆由插件贡献，Cordis 自身仍保留插件得以存在的机制。上下文代理、Fiber 状态机、服务存储、事件总线和 Loader 属于元层。</p>
<p data-tool="mdnice编辑器">这并不是系统里没有内核，更准确的说，应该是，内核不拥有模型、工具和循环等产品特权。</p>
<h1 data-tool="mdnice编辑器"><span class="content">空间组合</span></h1>
<p data-tool="mdnice编辑器">同一个进程里，服务名称必须稳定，实例又不能全局唯一。两个 Agent 可能选择不同模型、不同工具集、不同 persona 和不同沙箱。如果 <code style="color: #ef7060;">ctx.llm</code> 永远指向一个全局对象，插件替换只发生在进程级，无法满足多会话并存。</p>
<p data-tool="mdnice编辑器">Cordis 的 <code style="color: #ef7060;">Context.isolate</code> 为指定服务名创建一个 realm 标签。服务注册和查找都以该标签定位，同名服务可以在不同子上下文里各自存在。两个 <code style="color: #ef7060;">isolate</code> 调用传入同一标签时会加入同一 realm；使用新标签时彼此隔离。子上下文通过原型继承父上下文，隔离映射按层遮蔽，因此局部配置不需要复制整棵容器。</p>
<p data-tool="mdnice编辑器">这解释了「空间可组合」的第一层：组合具有位置。插件挂在哪个上下文，决定它提供的服务、注册的监听器和持有的副作用在哪个范围可见。传统依赖注入容器也有 singleton、request、session 等 scope，Cordis 的差异在于作用域与插件树、事件过滤、资源所有权使用同一个 Context 表达。开发者无需在四套 API 之间同步身份。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 又增加了 <code style="color: #ef7060;">dsh-scope</code>。它用不透明对象作为 scope key，维护父子关系，并创建带路由身份的事件 receiver。注册视图沿父链向下继承：Agent 能看到所属 preset 的提示词和工具。事件则沿链向上接纳：preset 级监听器能收到其下 Agent 的事件，兄弟 Agent 互不串线。这是第二层空间结构，解决的是领域身份，而非单个服务名的 realm。</p>
<p data-tool="mdnice编辑器">为何需要两层？<code style="color: #ef7060;">isolate</code> 处理「哪个 <code style="color: #ef7060;">tools</code> 服务实例」，<code style="color: #ef7060;">dsh-scope</code> 处理「同一工具注册表里，哪些注册项对当前 Agent 可见」。前者适合替换提供方，后者适合对注册内容分层。把所有差异都做成独立服务实例，会放大内存和初始化成本；把所有差异都塞进一个全局注册表，又会让过滤规则散落在调用点。这两层模型把实例隔离和内容路由分开了。</p>
<p data-tool="mdnice编辑器">Agent preset 是空间组合的完整应用。一个 preset 的 Cordis 配置会挂在一个长期存在的 scope 下，Agent 创建时把自己的 scope key 绑定到该 preset。相同 preset 的并发首次使用通过 single-flight 共享一次挂载，后续 Agent 复用这份组合。文件变化后，新会话加入新一代组合，已有会话保留原来的代际。这个策略避免会话运行中途突然更换工具或提示词，代价是旧代际要保留到整棵运行时退出，配置频繁变化时内存会按代际增长。</p>
<p data-tool="mdnice编辑器">源码还专门审计 preset 子树是否把服务发布到了 root realm。发生这种泄漏时，第二个会话挂载同一 preset 会与第一个冲突，所谓会话级组合也会退化成进程全局状态。DeepSeek Harness 在发布 Agent 前拒绝这种配置。空间隔离若只靠约定，迟早会被一个没有 <code style="color: #ef7060;">isolate</code> 的 provider 穿透；运行时审计比文档警告可靠。</p>
<p data-tool="mdnice编辑器">这套空间模型存在认知成本。插件作者要同时理解 Context 父链、服务 realm、业务 scope 父链和事件过滤方向（当然，在当前 Vibe Coding 盛行的时代，也可以作者不理解，直接让 AI 来搞）。</p>
<p data-tool="mdnice编辑器">在认知不清楚的时候，常见的错误往往表现为某项能力「看不见」或意外泄漏，类型系统无法证明运行时挂载位置。DeepSeek Harness 用 preset 挂载审计、包级 invariant 和真实组合测试降低风险，却没有消除模型复杂度。团队若只需要单进程单 Agent，直接引入整套空间语义会显得过重。</p>
<h1 data-tool="mdnice编辑器"><span class="content">时间组合</span></h1>
<p data-tool="mdnice编辑器">插件能挂载只是静态组合。运行期间服务上线、配置改变、插件失败或上下文销毁，系统还要回到一致状态。Cordis 用 Fiber 和 effect 管理这条时间轴。</p>
<p data-tool="mdnice编辑器">每次插件应用都会产生一个 Fiber。Fiber 记录父上下文、原始配置、解析后配置、依赖实现快照、生命周期状态和 disposables。插件调用 <code style="color: #ef7060;">ctx.effect()</code> 注册副作用，effect 的执行结果返回 disposer。事件监听、服务提供、子插件和访问器最终都进入这套所有权体系。Fiber 卸载时按注册的逆序执行清理，并等待异步清理达到静止状态。</p>
<p data-tool="mdnice编辑器">逆序本身不是一个特别要讲的事情，因为这是必须的。</p>
<p data-tool="mdnice编辑器">若插件先启动子进程，再注册输出监听，最后暴露服务，销毁时应先撤销服务，停止新请求，再移除监听，最后结束进程。资源创建顺序的反向通常就是依赖安全的拆卸顺序。</p>
<p data-tool="mdnice编辑器">传统插件常给出一个 <code style="color: #ef7060;">deactivate()</code> 钩子，把所有清理责任推给作者；漏掉一个定时器或事件监听，热加载几次便出现重复执行。Cordis 让每次注册同时产生撤销动作，框架持有所有权。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 的 vendor 版本进一步处理了重入场景：effect 在执行 setup 前先登记所有者包装；插件发布事件时，观察者可能同步卸载它；异步 cleanup 已经开始后，其他调用者仍能等待同一次清理；Fiber 处于 <code style="color: #ef7060;">UNLOADING</code> 时拒绝创建新 effect。这些代码看起来全是繁琐的边界处理，但为了保证生命周期并发的安全，一个都不能少。否则插件的热卸载就只是个玩具，没法真正落地。</p>
<p data-tool="mdnice编辑器">依赖变化也属于时间组合。某个服务被提供后，反射层通知所有声明该依赖的 Fiber 重新检查；条件满足便激活。服务撤销后，依赖方会卸载并回到等待。OSGi 早已采用动态服务注册表，Cordis 的新意主要在于把动态依赖、作用域上下文和 effect 所有权压进一个很小的进程内模型。代价同样继承自 OSGi：任何持有服务引用越过生命周期的代码，都可能在提供方撤销后继续调用过期对象。Cordis 保存加载时的实现快照，却无法替业务代码管理逃逸引用。</p>
<p data-tool="mdnice编辑器">Loader 把时间组合延伸到配置。DeepSeek Harness 的 profile 由多层 bundle patch 叠加：基础 bundle、模式 bundle、profile patch、home patch、命令行 overlay。patch 按 id 定位条目，配置采用整块替换。整块替换要求用户重述保留字段，使用上稍显笨重，却避免深度合并规则在数组、表达式和删除语义上制造歧义。</p>
<p data-tool="mdnice编辑器">配置热更新时，Loader 先导入候选插件，再卸载旧实例并应用候选；候选失败会恢复旧插件或旧配置。Group 对一批子条目并发启动，收集全部结果，只要一项失败就删除新增项并重建旧配置。Include 读取候选文件、在副本上应用 patch、完成树协调后才提交缓存。用户 patch 语法错误或新插件启动失败时，最后一棵可用树继续运行。</p>
<p data-tool="mdnice编辑器">这已经接近配置事务，却不能等同于数据库事务。</p>
<p data-tool="mdnice编辑器">插件 effect 可能调用外部 API、创建远端资源、发送消息；disposer 只能做补偿，无法保证外部世界回滚。Loader 能保证自己管理的树和服务注册恢复，无法撤回所有不可逆副作用。插件开发规范仍要限制初始化期行为，把外部写入延迟到真正的业务请求，或者设计幂等键和补偿路径。</p>
<p data-tool="mdnice编辑器">时间组合还带来可观的测试面积。挂载成功只是第一条路径，还要覆盖依赖晚到、依赖撤销、初始化失败、卸载重入、异步清理、热更新失败、回滚再次失败。DeepSeek Harness 要求注册项证明 disposal，产品可见插件还要通过真实 Loader 组合测试。这个成本无法靠框架消失，只能被框架集中暴露。相比线上出现幽灵监听器和半更新状态，还是愿意支付这部分测试费用。</p>
<h1 data-tool="mdnice编辑器"><span class="content">事件契约</span></h1>
<p data-tool="mdnice编辑器">插件之间只靠服务调用，会把所有扩展都变成接口方法。宿主每增加一项策略，就要修改服务定义。Cordis 同时提供类型化事件，并区分 <code style="color: #ef7060;">emit</code>、<code style="color: #ef7060;">parallel</code>、<code style="color: #ef7060;">serial</code>、<code style="color: #ef7060;">bail</code> 和 <code style="color: #ef7060;">waterfall</code>。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 最依赖的是 waterfall。它的监听器拿到 <code style="color: #ef7060;">next</code>，调用后把控制权交给下一层，不调用就截断链条。模型请求、工具执行和轮次控制都能被插件包裹。审批策略可以在工具执行前拒绝，重试插件可以包围模型流，日志插件可以观察前后状态。它与 Koa 一类中间件链相似，但事件名和 Context 过滤让同一个分发器覆盖多个能力域。</p>
<p data-tool="mdnice编辑器">waterfall 也最容易出错。一个只想记录日志的监听器忘记调用 <code style="color: #ef7060;">next()</code>，整条能力链便被短路。DeepSeek Harness 把「必须调用 <code style="color: #ef7060;">next()</code>」写进项目级规则，并在事件文档里记录 dispatch mode。类型能保证参数，却很难保证 continuation 一定执行。代码评审和组合测试仍是主要防线。</p>
<p data-tool="mdnice编辑器">事件的持久性也被刻意分层。<code style="color: #ef7060;">agent/*</code> 和 <code style="color: #ef7060;">tools/*</code> 事件用于活跃运行时的拦截；会话事件追加到日志，承担恢复、fork、UI 回放和模型历史投影。DeepSeek Harness 规定「模型可见即已记录」：进入模型请求的输入必须能从 session log 重建。插件若偷偷修改 prompt 却不产生会话事实，重放结果会漂移，问题也无法审计。</p>
<p data-tool="mdnice编辑器">这条约束说明，一切皆插件并不意味着一切皆事件。直接能力调用放进 Service，策略和拦截放进实时事件，需要持久化的事实进入 session log。三类通信各自承担调用、扩展和历史。将它们混成一个全局 event bus，短期代码更少，长期会失去时序语义和数据权威。</p>
<h1 data-tool="mdnice编辑器"><span class="content">Agent 适配</span></h1>
<p data-tool="mdnice编辑器">Agent Harness 比普通 Web 服务更需要动态组合。模型供应商会变，工具权限随工作区变化，子 Agent 的执行环境可能与父 Agent 不同，UI 还要消费同一份流式过程。如果主循环直接 import 每个能力，任何替换都会改循环；循环逐渐成为依赖最多、风险最高的文件。</p>
<p data-tool="mdnice编辑器">DeepSeek Harness 的默认 agent loop 仍然存在，但它也是一个服务提供方。循环读取 session log 生成历史，组装插件贡献的 prompt 和 tool schema，通过 <code style="color: #ef7060;">agent/request</code> 进入模型适配器，再经过工具流水线记录结果。扩展点围绕步骤、请求、工具和停止阶段布置。新功能通常挂到这些事件或注册表，项目规则甚至要求修改 agent loop 时同步更新架构文档。</p>
<p data-tool="mdnice编辑器">我赞成这种限制。循环承担控制流，频繁承载产品策略后会迅速腐化。压缩上下文、重试、审批、工具超时、计划模式、子 Agent 调度都可以有自己的生命周期与配置。它们进入循环的接口必须少且稳定。插件化不会自动获得解耦，真正起作用的是 DeepSeek Harness 为能力选择了明确的 seam，并拒绝消费方特有逻辑污染定义层。</p>
<p data-tool="mdnice编辑器">UI 作为插件也有实际意义。Web 和 headless profile 在共享 base bundle 上增加不同条目，后者可以完全不启动服务器。UI 驱动 <code style="color: #ef7060;">ctx.agents</code>，订阅 <code style="color: #ef7060;">session/event</code> 渲染状态，没必要成为循环的一部分。服务端、CLI、ACP 和浏览器界面可以共享会话及 Agent 语义，同时保留自己的传输和展示逻辑。</p>
<p data-tool="mdnice编辑器">插件树还提供自省基础。Fiber 保留名称、状态、依赖和 effect 元数据，DeepSeek Harness 能实现查看、挂载、卸载自身插件的能力。对 Agent 而言，这比普通应用更敏感：模型可以通过工具改变运行时，错误配置可能直接扩大权限。源码中的 sandbox 和 approval 仍是独立能力，插件架构没有天然安全性。自修改必须受工具权限、配置校验和作用域审计约束。</p>
<h1 data-tool="mdnice编辑器"><span class="content">架构代价</span></h1>
<p data-tool="mdnice编辑器">第一项代价是启动与运行时开销。每个插件产生 Fiber，服务访问经过 Proxy 和反射解析，事件分发要执行作用域过滤，注册项还要保存 disposer 与诊断元数据。对于 LLM Agent，单次模型请求通常以百毫秒到秒计，这些 JavaScript 调度开销很难成为主要瓶颈；高频 token chunk、文件扫描或终端字节流若全部穿过通用事件总线，成本会被放大。DeepSeek Harness 把流式模型输出定义为能力语义，但大量数据处理仍应留在具体 provider 内，事件只承载必要扩展点。</p>
<p data-tool="mdnice编辑器">第二项代价是故障面扩大。插件初始化可以同步抛错、异步拒绝、等待缺失服务，也可能在 disposer 中失败。Group 并发启动缩短时间，却会带来多个兄弟同时失败的 AggregateError。DeepSeek Harness 的 boot 会等待 Loader 结算，审计所有启用条目是否加载与激活，失败时先 dispose 部分构造的上下文，再退出。少做任何一步，都可能让终端停留在 raw mode，或者让后台 watcher 继续运行。</p>
<p data-tool="mdnice编辑器">第三项代价是配置成为编程接口。<code style="color: #ef7060;">cordis.yml</code> 支持 <code style="color: #ef7060;">!!js</code> 表达式，条目可以按环境禁用，配置会在依赖激活后求值。灵活性很高，静态分析能力随之下降。表达式读取服务时，求值时机与上下文位置都会影响结果。DeepSeek Harness 只允许 <code style="color: #ef7060;">config</code> 和 <code style="color: #ef7060;">disabled</code> 使用插值，其他元数据保持字面量，并让错误尽早暴露。这仍要求运维人员理解插件依赖和 patch 覆盖语义，配置文件已经超出普通 YAML 参数表的复杂度。</p>
<p data-tool="mdnice编辑器">第四项代价是生态兼容。Cordis 的服务名和事件类型提供了源码级协议，插件版本升级仍可能修改配置、事件 payload 或生命周期假设。DeepSeek Harness 目前处于预发布阶段，仓库规则允许直接拒绝旧磁盘格式，也没有承诺 session 格式兼容。现在的可组合性主要服务于同一发行版内的替换和扩展，尚不能推导出跨版本插件 ABI 稳定。</p>
<p data-tool="mdnice编辑器">第五项代价是组织治理。一切都能成为插件后，团队容易把每段十几行逻辑都拆成包，形成依赖图膨胀、文档分散和测试启动缓慢。DeepSeek Harness 用 package 分组、Service Definition/Provider/Consumer 角色、每包 README、运行时 invariant 和真实组合测试控制边界。这些规范本身就是成本。小团队若没有维护扩展生态或多 profile 的需求，模块化函数加显式依赖可能更合适。</p>
<h1 data-tool="mdnice编辑器"><span class="content">历史对照</span></h1>
<p data-tool="mdnice编辑器">从 OSGi 看 Cordis，动态服务和生命周期联动已有先例。服务注册、发现、撤销后触发依赖变化，这条主线几乎一致。Cordis 值得学习的新点，是把资源清理统一成 effect，并让子插件、服务、监听器都归属于同一个 Fiber。OSGi 的 bundle 生命周期更完整，也更重；Cordis 选择应用内细粒度对象，失去了类加载隔离、标准版本解析和安全层，获得了低门槛组合。</p>
<p data-tool="mdnice编辑器">从 Eclipse 看 DeepSeek Harness，profile 与 bundle patch 很像部署时组装，Service 和事件类似扩展点。差异在于 Eclipse 扩展通常围绕稳定宿主能力，DeepSeek Harness 连 agent loop 和 UI 都可替换，核心插件与第三方插件共享同一挂载机制。这个平权减少了特权内核，风险也更集中到协议治理：循环能被替换，不代表任何替代循环都遵守 session log、权限和工具时序约束。</p>
<p data-tool="mdnice编辑器">从依赖注入容器看，Cordis 的服务解析并不陌生。新的组合来自 DI 与生命周期的绑定。普通容器负责构造对象，定时器、监听器和子进程仍由业务代码清理；Cordis 把注册动作收敛成 effect。空间 scope 与时间 ownership 同时存在，插件才能在某个 Agent 范围内挂载，并随该 Agent 完整撤销。</p>
<p data-tool="mdnice编辑器">从微内核看，DeepSeek Harness 的内核确实很轻，但其 Loader 和配置协调已经承担不少平台职责。它要解析模块、等待依赖、处理 HMR、执行事务式更新、保存最后可用树、输出诊断。这提醒我，微内核减少的是业务特权，不会减少生命周期复杂度。能力越动态，内核对失败语义的要求越高。</p>
<p data-tool="mdnice编辑器">Cordis 的贡献可以概括为一种紧凑的组合坐标：Context 决定插件位于哪里，Fiber 决定插件在何时有效，Service 负责直接能力，Event 负责横切协作，effect 负责撤销。DeepSeek Harness 在这个坐标系上加入 Agent scope、持久会话事件和配置事务，使其能承载多会话、多 profile 与热更新。</p>
<h1 data-tool="mdnice编辑器"><span class="content">工程取舍</span></h1>
<p data-tool="mdnice编辑器">当我想要用类似架构时，会先确认三个条件，基于这三个条件判断来是否采用。</p>
<p data-tool="mdnice编辑器">其一，能力确实需要独立替换，至少存在两个生产提供方或明确的外部扩展需求。</p>
<p data-tool="mdnice编辑器">其二，同一进程需要多套组合并存，或者运行期间需要可靠更新。</p>
<p data-tool="mdnice编辑器">其三，团队愿意为卸载、回滚和真实组合测试持续付费。</p>
<p data-tool="mdnice编辑器">缺少这些条件，插件框架容易沦为复杂的工厂模式。</p>
<p data-tool="mdnice编辑器">能力切分要从消费方开始。先列出谁调用、调用时需要哪些稳定语义，再设计 Service Definition；随后实现 provider，最后把面向模型的 schema、展示和错误文本留给 consumer。接口只有一个内部调用者时，保留私有闭包，不急着升格为公共 service。可替换性必须由真实替代者证明。</p>
<p data-tool="mdnice编辑器">所有注册都要有明确所有者。注册工具、提示词片段、适配器、监听器时同步返回 disposer；卸载测试要观察注册项确实消失。涉及异步资源时，dispose 完成的定义要包含子进程退出、队列排空和 watcher 关闭，不能只发出取消信号。DeepSeek Harness 对 Fiber quiescence 的处理值得照搬。</p>
<p data-tool="mdnice编辑器">配置更新要区分内存一致性和外部副作用。Loader 可以恢复旧树，插件初始化若已经创建云资源，回滚需要业务补偿。规则可更保守一些：挂载阶段只做校验和本地注册，外部写操作放到带幂等标识的运行阶段。确实要在初始化创建资源时，必须让 disposer 能识别部分完成状态。</p>
<p data-tool="mdnice编辑器">作用域要控制在两种以内。实例隔离与注册可见性已经足够表达多数 Agent 场景，再增加租户、请求、工作区、角色四套独立 scope，调试成本会失控，需要根据实际业务需求和必要性来判断。当需要新增维度时，先判断它属于服务实例选择、注册过滤、持久数据权限还是请求参数。很多所谓新 scope，其实只是一次显式参数传递。</p>
<p data-tool="mdnice编辑器">事件也要克制。需要唯一返回值的调用用 Service，需要持久恢复的事实写 session log，需要多个插件观察或包裹的阶段才用事件。waterfall 必须把短路当成公共协议，监听器顺序也要有测试。事件名虽然能降低导入耦合，却会增加时序耦合；后者通常更难排查。</p>
<h1 data-tool="mdnice编辑器"><span class="content">小结</span></h1>
<p data-tool="mdnice编辑器">DeepSeek Harness 选择 Cordis，解决的主要矛盾是 Agent 能力变化速度与运行时一致性之间的冲突。模型、工具、沙箱和 UI 都可以替换并不稀奇；这些能力可以在局部作用域内共存，能随依赖变化启停，能在配置失败后保留旧树，卸载时还能回收监听器、服务和子插件，才构成可用的插件架构。</p>
<p data-tool="mdnice编辑器">它也没有抹平工程风险。Proxy 隐藏了查找路径，双层 scope 增加理解成本，热更新无法回滚任意外部副作用，预发布阶段也缺少跨版本兼容承诺。团队采用这套思路时，应复制它的生命周期纪律和能力边界，别只复制「一切皆插件」的目录结构。</p>
<p data-tool="mdnice编辑器">源码给我们一些启发，把插件设计的评审顺序倒过来：先问如何撤销，再问如何注册；先证明局部挂载不会泄漏，再讨论全局复用；先定义失败后保留哪一棵树，再讨论热更新速度。做到这些，时空可组合才是一组可以验证的运行时语义。</p>
<p data-tool="mdnice编辑器">这套架构个人理解是想往 Agent 的实时「自生长」，通过 AI 的能力在使用过程中让自己更强大。</p>
<p data-tool="mdnice编辑器">逼逼这么多，主要还是在这个过程中学习一下，说实话，有了 DeepSeek Harness，想自己开发一个专属的 Agent ，快捷方便了很多，eg且这是 MIT 协议的。</p>
<p data-tool="mdnice编辑器">以上。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #ffffff;">参考资料</span></h2>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;"><a style="font-weight: bold; color: #ef7060;" href="https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.zh.md">DeepSeek Harness 架构文档</a></section>
</li>
<li>
<section style="color: #010101;"><a style="font-weight: bold; color: #ef7060;" href="https://github.com/cordiverse/cordis">Cordis：Meta-Framework of Spatiotemporal Composability</a></section>
</li>
<li>
<section style="color: #010101;"><a style="font-weight: bold; color: #ef7060;" href="https://osgi.github.io/osgi/core/framework.service.html">OSGi Service Layer</a></section>
</li>
<li>
<section style="color: #010101;"><a style="font-weight: bold; color: #ef7060;" href="https://www.osgi.org/resources/architecture/">OSGi Architecture</a></section>
</li>
<li>
<section style="color: #010101;"><a style="font-weight: bold; color: #ef7060;" href="https://www.eclipse.org/articles/Article-Plug-in-architecture/plugin_architecture.html">Eclipse Plug-in Architecture</a></section>
</li>
</ul>
</section>
<p>&nbsp;</p>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/08/deepseek-harness-cordis/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>做 Agent 架构时，OpenViking 的几个可以学习的点</title>
		<link>https://www.phppan.com/2026/07/agent-openviking/</link>
		<comments>https://www.phppan.com/2026/07/agent-openviking/#comments</comments>
		<pubDate>Sun, 26 Jul 2026 03:59:43 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[OpenViking]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2521</guid>
		<description><![CDATA[设计 Agent 架构时，有一个模块的边界经常画不清晰：上下文。规划、工具调用、执行循环这些组件的职责都好切分 [&#8230;]]]></description>
				<content:encoded><![CDATA[<p style="color: #191b1f;" data-first-child="" data-pid="EAu8Ffnz">设计 Agent 架构时，有一个模块的边界经常画不清晰：上下文。规划、工具调用、执行循环这些组件的职责都好切分，唯独「Agent 该知道什么」这件事，散落在 prompt 模板、RAG 管道、记忆插件和会话历史四个地方，各管一段，互不通气。</p>
<p style="color: #191b1f;" data-pid="aMUyz_Cj">比较常见的状态是知识走一条 RAG 管道，用户记忆走一个独立的 memory 服务，工具使用经验干脆没有沉淀机制，靠 system prompt 里手写的几条注意事项撑着。跑长周期任务时问题集中爆发：第三十轮时 Agent 忘了第五轮的约束；召回了一段错误文档，事后想归因，向量库只给一个相似度 0.83，别的什么都没有。</p>
<p style="color: #191b1f;" data-pid="el4iar4w">这两个 case 背后是同一个架构缺陷：上下文没有被当作一个独立子系统来设计，它只是若干条数据管道的松散集合。</p>
<p style="color: #191b1f;" data-pid="-LSVd0qA">最近把火山引擎开源的 OpenViking 看了一遍，它把自己定位成「面向 AI 智能体的上下文数据库」，GitHub 上 27.2k star。这个定位本身就是一个架构主张：上下文应该是 Agent 架构里一个有统一接口、有存储模型、有检索语义、可观测的独立组件，地位类比业务系统里的数据库。</p>
<p style="color: #191b1f;" data-pid="cvQ_eRuL">沿着这个主张往下拆，它有几个设计决策对做 Agent 架构的人有一些参考价值。</p>
<h2 style="font-weight: 500; color: #191b1f;">统一存储模型</h2>
<p style="color: #191b1f;" data-pid="OXbI9IFm">Agent 架构里第一个要回答的问题：记忆、知识、技能这三类上下文，存储模型是分开还是统一。</p>
<p style="color: #191b1f;" data-pid="_C15LbGR">分开是多数团队的现状，也是我们踩过的坑——三套 schema、三套检索接口、三套更新逻辑，Agent 侧要维护三种访问心智。OpenViking 的选择是统一：全部抽象成文件，挂在 <code>viking://</code> 协议下的虚拟文件系统里，每个条目一个唯一 URI。它给出的目录结构：</p>
<div class="highlight" style="color: #191b1f;">
<pre><code class="language-text">viking://
├── resources/              # 资源：项目文档、代码库、网页等
│   └── my_project/
│       ├── docs/
│       │   ├── api/
│       │   └── tutorials/
│       └── src/
└── user/
    └── {user_id}/
        ├── memories/
        │   └── preferences/
        │       ├── writing_style
        │       └── coding_habits
        ├── resources/
        │   └── private_project/
        ├── skills/
        │   ├── search_code
        │   └── analyze_data
        └── peers/
            └── web-visitor-alice/</code></pre>
</div>
<p style="color: #191b1f;" data-pid="vT6qFbFR">Agent 用 <code>ls</code>、<code>tree</code>、<code>find</code> 浏览自己的上下文。</p>
<p style="color: #191b1f;" data-pid="2Yhb04Wj">从架构角度来看，这个统一带来两个收益。第一，定位从概率性变成确定性。扁平向量库里想精确更新某个用户的编码习惯记忆，只能靠 metadata filter 打补丁；文件系统里就是一个路径寻址，<code>viking://user/{user_id}/memories/preferences/coding_habits</code>，增删改查全部确定。多租户隔离也顺带解决了——<code>user/{user_id}</code> 这层目录天然就是隔离边界，商业版文档里明确把多租户共享和隔离列为核心场景。</p>
<p style="color: #191b1f;" data-pid="PgrcTj5b">第二，Agent 与上下文之间的接口收敛成了一套文件操作原语。这一点对架构的影响比看起来大。我们之前自研记忆系统时设计过一套 <code>memory.query(scope, filter, ...)</code> 风格的 API，模型调用的出错率明显高于让它直接操作路径。文件系统是 LLM 预训练语料里高频出现的心智模型，<code>ls</code> 和 <code>tree</code> 的语义模型天然理解，不需要在 prompt 里教。接口设计顺着模型先验走，省下的是持续的 prompt 工程成本。</p>
<p style="color: #191b1f;" data-pid="bMOXDRk6">代价在写入侧的分类决策。内容归置到哪个路径，做错了后续检索会系统性偏航，扁平向量库至少没有「放错文件夹」这种失败模式。OpenViking 靠写入时的自动语义处理来做归置，这个环节的质量上限决定了整棵文件树的可用性，我认为是它工程上最脆弱的一环。</p>
<h2 style="font-weight: 500; color: #191b1f;">预算分层供给</h2>
<p style="color: #191b1f;" data-pid="mHGZ2zN3">第二个架构问题：上下文以什么粒度供给给模型。</p>
<p style="color: #191b1f;" data-pid="H33IvGor">传统 RAG 的答案是 chunk 全量——检索 top-k 塞进 prompt，k 靠拍脑袋。这在架构上等于把上下文预算的管理责任丢给了一个魔法数字。OpenViking 的方案是写入时预计算三层表示：</p>
<ul style="color: #191b1f;">
<li data-pid="Tmtm70Kr">L0（摘要）：约 100 tokens，一句话总结，快速判断相关性</li>
<li data-pid="4oKtO6wj">L1（概览）：约 2k tokens，核心信息和使用场景，供规划阶段决策</li>
<li data-pid="-APyM9Yd">L2（详情）：完整原始数据，确有必要时才读</li>
</ul>
<p style="color: #191b1f;" data-pid="zBH3NtWe">每个目录也带自己的 <code>.abstract</code> 和 <code>.overview</code>，Agent 读任何完整文件之前，先花 100 个 token 判断这个方向值不值得深入。</p>
<p style="color: #191b1f;" data-pid="4-aXPxb3">放到 Agent 的执行循环里看，这三层恰好对应了三个阶段的信息需求：候选筛选阶段用 L0 扫一遍，规划阶段对少数目标加载 L1，执行阶段只对必要条目读 L2。上下文供给从一次性的赌博变成了跟随 Agent 决策流程的渐进加载，粒度和阶段对齐了。</p>
<p style="color: #191b1f;" data-pid="obq4ONV6">数据上，OpenViking 0.3.22 在 LoCoMo 长对话记忆评测里，三种 Agent 集成的准确率到 80–83%，原生记忆只有 24–57%；输入 token 减少 34.3%–91.0%，查询时延降低 58.45%–66.10%。token 降幅区间这么宽，说明收益高度依赖任务形态：判断型任务能省九成，需要全量细节的任务省不了多少。评测脚本在仓库 <code>./benchmark</code> 目录，可自行复现。</p>
<p style="color: #191b1f;" data-pid="ZChgUhTf">架构上的 trade-off 转移到了写入路径。L0/L1 由模型生成，每次数据摄入都有一笔推理开销，且是异步的——README 明确提示 <code>add-resource</code> 不加 <code>--wait</code> 时语义处理需要等待。写入吞吐敏感或数据高频变更的场景，这条路径会成为瓶颈。更隐蔽的风险是摘要写歪：L0 错了，后续所有基于它的相关性判断连带出错，等于在检索链路最上游埋单点。官方提供 <code>ov reindex &lt;uri&gt; --mode semantic_and_vectors</code> 重新生成语义产物再刷新向量，说明他们自己也预期摘要需要返工。</p>
<p style="color: #191b1f;" data-pid="zcid3ZaV">即便不引入 OpenViking，「写入时预计算多粒度表示、按 Agent 决策阶段供给」这个模式可以直接抄进自建架构。</p>
<h2 style="font-weight: 500; color: #191b1f;">检索即导航</h2>
<p style="color: #191b1f;" data-pid="kSImNmZF">检索组件在 Agent 架构里通常被实现成一个无状态函数：query 进，top-k 出。OpenViking 把它改成了一个有过程的导航。</p>
<p style="color: #191b1f;" data-pid="FMfkO849">先通过意图分析生成多个检索条件；向量检索定位初始切片所在的高分目录；在该目录下二次检索，高分结果更新进候选集；有子目录就逐层递归；最终返回最相关的上下文，连同周边语境一起。</p>
<p style="color: #191b1f;" data-pid="JeNy67s-">一句话概括：先锁定高分目录，再向下精细探索。</p>
<p style="color: #191b1f;" data-pid="QKso_oQd">这解决的是扁平 top-k 在 Agent 场景下的一个结构性缺陷——碎片没有语境。命中一个 chunk，Agent 不知道它前后是什么、属于哪份文档的哪个章节，拿到的是信息孤岛。导航式检索的返回结果自带位置和邻居：这段内容出自 API 文档的鉴权章节，同目录下还有 endpoints 说明。对代码库问答这类强依赖结构的任务，这个差异是决定性的。</p>
<p style="color: #191b1f;" data-pid="cH_9ytDz">代价：多轮向量查询加上可能的多次判断，单次检索的绝对延迟一定高于一把梭的 ANN。前面提到的时延降低 58%–66%，我的理解是端到端口径——省下的 token 让生成阶段变快，摊平了检索阶段的开销。架构选型时要按流量形态判断：Agent 任务型场景，单次任务价值高、容忍秒级检索，划算；高 QPS 低延迟的在线检索，这套递归策略不合适。</p>
<h2 style="font-weight: 500; color: #191b1f;">全链路留痕</h2>
<p style="color: #191b1f;" data-pid="lmAUpM13">Agent 架构的可观测性讨论，通常止步于工具调用日志。OpenViking 把留痕做进了检索内部：每次查询都保留完整的目录浏览轨迹，结果不对时能看到它出自哪条路径。</p>
<p style="color: #191b1f;" data-pid="dfFSb1CA">做过 RAG 生产运维的人知道 bad case 归因有多难。用户投诉答案错了，排查下来只能看到「这几个 chunk 相似度最高所以被召回」，正确内容为什么没进 top-k，向量空间不会解释。最后退化成调 embedding、调 chunk 大小、调 k 值的炼丹循环。</p>
<p style="color: #191b1f;" data-pid="sEX8qK4H">轨迹留存把归因变成可逐步 debug 的过程：检索在哪个目录拐错了弯、哪一层的 L0 摘要误导了判断，路径上每一步可查。修复动作随之有了着力点——重写某个目录的 abstract，或者调整文件归置，都对应到具体操作。配套的 OpenViking Helper 桌面端（Beta，支持 macOS 和 Windows x64）能解析 Claude Code、Codex、Trae 的会话，展示召回、Prompt 注入、MCP 调用、捕获和提交事件，Agent 与上下文系统之间的全部交互摆在明面上。</p>
<p style="color: #191b1f;" data-pid="wMsAyqnE">这一条我建议所有做 Agent 基础设施的团队照抄，无论用不用 OpenViking：把「检索决策可回放」写进架构的非功能需求。存储和埋点的成本，第一次生产事故排查时就能收回来。</p>
<h2 style="font-weight: 500; color: #191b1f;">经验回写闭环</h2>
<p style="color: #191b1f;" data-pid="uIPUFTYq">多数 Agent 架构是开环的：任务执行完，除了对话历史什么都不留，下次遇到同类任务从零开始。OpenViking 在架构里补了一条回写路径：会话通过 <code>session.commit()</code> 显式提交后，系统异步提取两类内容写入长期记忆——用户偏好，和 Agent 经验，落到对应的 <code>/memory</code> 目录下。</p>
<p style="color: #191b1f;" data-pid="NcKzWSYU">用户偏好各家都在做。Agent 经验这条更值得看：从任务执行结果里提取操作技巧、工具使用经验，供后续同类任务复用。tau2-bench 的数据：经验记忆让任务成功率在 Retail 提升 6.87 个百分点，Airline 提升 11.87 个百分点，对比同一 LLM 无记忆基线。模型没动，纯靠架构里加一条经验沉淀回路，拿到两位数以内的成功率增量。</p>
<p style="color: #191b1f;" data-pid="8zzpDbGT">对比另一条提升路线——攒数据做 SFT 或 RL 微调，周期以周计，成本以万计。经验记忆改的是上下文而非权重，迭代周期是每次会话。两条路线不互斥，但在架构演进的排序上，外置经验回路的边际成本低得多，应该排在微调前面。</p>
<p style="color: #191b1f;" data-pid="IVvbBGO8">两处要留神。一是提取质量：异步抽取本身是 LLM 调用，抽错等于往长期记忆注入脏数据，且脏记忆会在后续任务里持续生效，危害大于一次性的错误回答。二是显式 commit 这个设计——记忆更新由开发者主动触发而非全自动，「哪些会话值得沉淀」的裁量权留在应用层。我们内部一度想做全自动沉淀，现在倾向于保留这个开关，至少在提取质量有验证机制之前。</p>
<h2 style="font-weight: 500; color: #191b1f;">接入成本</h2>
<p style="color: #191b1f;" data-pid="65NYtME-">把这套东西放进现有 Agent 架构的成本，不高。</p>
<p style="color: #191b1f;" data-pid="7MYbVe1l">Python 3.10 以上，<code>pip install openviking --upgrade</code>，<code>openviking-server init</code> 走交互式向导——支持火山引擎、OpenAI、Codex OAuth、Kimi、GLM 和本地 Ollama，选 Ollama 还能按硬件拉合适的模型。<code>openviking-server doctor</code> 启动前体检配置、Python 版本、提供商连通性和磁盘空间。有官方 Docker 镜像和 Helm 部署。</p>
<p style="color: #191b1f;" data-pid="8KfHXdrJ">集成面很宽：Claude Code、Codex、OpenClaw、Hermes、Cursor、Trae、OpenCode、pi、MCP 客户端、LangChain / LangGraph 都有官方接入，集成做的事是把召回注入 Agent 上下文并自动提交会话记忆。如果你的架构本来就走 MCP 或 LangGraph，接入成本主要在配置层。</p>
<p style="color: #191b1f;" data-pid="sC2OiHZG">许可证要过法务。主项目 AGPLv3，<code>crates/ov_cli</code> 和 examples 是 Apache 2.0。AGPLv3 自用没问题，基于它对外提供网络服务会触发传染性条款的开源义务。商业版 OpenViking Service 承诺与开源版共享同一套代码内核，差异在托管运维、基于 VikingDB 的规模（宣称万亿级向量、百亿数据毫秒级检索）、SLA 和企业级安全合规，有至多 50 个文件的免费试用和迁移工具。开源版默认本地存储加单机向量引擎，数据规模上去后的可靠性要自己扛，架构容量规划时要计入。</p>
<p style="color: #191b1f;" data-pid="e6027xiq">背后有论文支撑：VikingMem（arXiv:2605.29640），已被 VLDB 2026 接收，开源版实现了论文描述的部分核心能力。团队在按数据库系统的标准做存储和检索设计，这在 Agent 记忆类项目里少见。</p>
<h2 style="font-weight: 500; color: #191b1f;">需要注意的点</h2>
<p style="color: #191b1f;" data-pid="NtGLKL_E">三点。</p>
<p style="color: #191b1f;" data-pid="XnhJGLus">其一，整个体系的地基是写入时的自动语义处理——摘要、概览、目录归置全靠模型生成，这些产物没有质量下界。LoCoMo 和 tau2-bench 覆盖的场景有限，换到术语密集的企业知识库，L0 摘要的准确率会不会崩，要拿自己的真实数据验证，别信 benchmark 直接上生产。</p>
<p style="color: #191b1f;" data-pid="GQmCvP6C">其二，导航式检索的延迟特性划定了适用边界，架构选型时按流量形态判断，前面说过了。</p>
<p style="color: #191b1f;" data-pid="wNBh-R4r">其三，项目还早，README 自己承认「要做的事还很多」。1813 次 commit、92 个 open issue、320 个 PR，迭代活跃，反面是 API 稳定性存疑，核心链路重度依赖它要做好跟版本的准备。</p>
<p style="color: #191b1f;" data-pid="89X_UDoy">回到 Agent 架构本身。「上下文作为独立子系统」「统一文件存储模型」「按决策阶段分层供给」「检索决策可回放」「经验回写闭环」，这五个设计每一个都能脱离 OpenViking 独立存在。</p>
<p style="color: #191b1f;" data-pid="bVp_k3oJ">以上</p>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/07/agent-openviking/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Session First 和 Agent First，本身并没有高低之分</title>
		<link>https://www.phppan.com/2026/07/session-first-and-agent-first/</link>
		<comments>https://www.phppan.com/2026/07/session-first-and-agent-first/#comments</comments>
		<pubDate>Sun, 05 Jul 2026 14:49:59 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Agent]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2515</guid>
		<description><![CDATA[最近在思考一个问题：系统究竟把谁当成第一类公民。 现在我们看到很多的 AI 产品，，本质上还是「把聊天框做得更 [&#8230;]]]></description>
				<content:encoded><![CDATA[<p style="color: #191b1f;" data-first-child="" data-pid="hn-QCbaX">最近在思考一个问题：系统究竟把谁当成第一类公民。</p>
<p style="color: #191b1f;" data-pid="ptsNuLrP">现在我们看到很多的 AI 产品，，本质上还是「把聊天框做得更厚一点」。用户打开一个窗口，对着通识大模型说需求，模型在一个长 session 里记上下文、调几个工具、写几段内容、偶尔执行点操作。这个模式我称它为「Session First」。</p>
<p style="color: #191b1f;" data-pid="LXs8VSfS">「Session First」的模式跑得起来，落地也快。过去一段时间，桌面端的很多产品，包括一些 codex cc 一类的形态，都是这么长出来的。先有聊天，再把能力缝进去。先有 session，再把工具挂上去。整个产品又快又省，因为它继承的是大模型最自然的交互方式：对话。</p>
<p style="color: #191b1f;" data-pid="xYQONgUk">但如果把时间线拉长，我觉得 Session First 像是一个过渡层。它把大模型带进软件，却没有真正重写软件。</p>
<p style="color: #191b1f;" data-pid="KEgmjjTl">另一个逻辑是 「Agent First」。</p>
<p style="color: #191b1f;" data-pid="7RKbj9VV">这里说的 Agent First，不是把 prompt 包一层 workflow 就算 agent。这里说的是一种更彻底的软件设计取向：系统从一开始就假设调用者不只是人，也包括 agent；系统的能力边界、接口形态、文档组织、安全机制、运行时观测，都优先围绕 agent 来设计。聊天只是入口之一，不再是核心结构。</p>
<p style="color: #191b1f;" data-pid="VVmbLg7b">过去我们是在和「通识大模型」对话，未来更多时候，我们是在和「带有领域知识、工具能力、任务规划能力的专用 agent」协作。再往前走，单一 agent 很可能都不够，基于 MaaS 的 agent teams 会逐步成为复杂任务的常态。Sakana AI 这类工作给了行业一个很有代表性的信号：多智能体协作，不只是研究兴趣，它在复杂探索任务上确实可能优于单体。</p>
<p style="color: #191b1f;" data-pid="MfTI3kQd">这种两种不同的范式，其实也没有高级和低级之分。二者背后的系统假设不同，工程代价不同，性能瓶颈不同，能解决的问题类型也不同。</p>
<h2 style="font-weight: 500; color: #191b1f;">Session First 和 Agent First 的不同</h2>
<p style="color: #191b1f;" data-pid="NZsnKq2A">Session First 和 Agent First，表面上都可能长得像「用户输入一句话，系统输出一个结果」。很多人可能会觉得两者只是包装差异。其实差异蛮大的。</p>
<p style="color: #191b1f;" data-pid="ab7mBUZ1">Session First 的中心对象是「会话」。系统假设一切能力都围绕一个持续增长的上下文展开。用户说一句，模型接一句；模型靠历史消息理解意图，靠当前 prompt 决定行为。工具调用存在，但工具通常是会话的附属物。它们由模型在 session 内临时选择、临时编排、临时解释。</p>
<p style="color: #191b1f;" data-pid="nSHdRCaa">所以在 Session First 里，能力组织方式通常是这样的：</p>
<ol style="color: #191b1f;">
<li data-pid="5eq_-0Tu">先有一个大而全的聊天入口；</li>
<li data-pid="9jCgn5ps">再有一组工具函数；</li>
<li data-pid="BPUP9XxE">再在 system prompt 或 tool spec 里告诉模型什么时候调用它们；</li>
<li data-pid="wYj2UQTX">复杂任务靠更长的上下文、更复杂的提示词、更精细的 few-shot 去兜。</li>
</ol>
<p style="color: #191b1f;" data-pid="Jvrm8KC3">这个模式最大的好处，是产品和研发都能快速起跑。我们不需要先把系统抽象成可组合能力，也不需要先考虑 agent 的长期运行、恢复、权限边界、状态持久化。只要模型够强、上下文够长、工具挂得上去，很多事情都能先做出来。</p>
<p style="color: #191b1f;" data-pid="eiiIaM52">Agent First 的中心对象则是「行动体」。系统假设执行任务的主体是 agent，它会自己读取规范、规划步骤、调用工具、检查结果、重试修正。人在这个系统里依然重要，但人不再是唯一的控制中心。更准确地说，系统从「为人类交互而生」转向「为可委托执行而生」。</p>
<p style="color: #191b1f;" data-pid="8Fh6-t08">这个变化会连锁改写很多设计决策。</p>
<p style="color: #191b1f;" data-pid="NLaUuQ5D">传统软件里，人是第一类公民。系统主要通过 UI 提供能力，文档写给人看，按钮给人点，异常信息也默认人会读。Agent First 里，agent 变成第一类公民。系统的关键界面不再是页面和按钮，而是 API、工具协议、语义化描述、状态机、权限模型、回调事件、执行日志。</p>
<p style="color: #191b1f;" data-pid="pgeDZ4to">传统模式是：</p>
<ul style="color: #191b1f;">
<li data-pid="J6fBCe4D">人读文档；</li>
<li data-pid="TGKaTkKs">人理解业务逻辑；</li>
<li data-pid="L5PkGFvA">人决定点哪个按钮；</li>
<li data-pid="SQC_RLB3">人承担串联流程的责任。</li>
</ul>
<p style="color: #191b1f;" data-pid="MGPdMjHs">Agent 模式是：</p>
<ul style="color: #191b1f;">
<li data-pid="guhaOQ-O">Agent 读取规范；</li>
<li data-pid="PD18qokp">Agent 形成计划；</li>
<li data-pid="muoTtpZi">Agent 调用工具；</li>
<li data-pid="vzGt_6af">Agent 分析返回值；</li>
<li data-pid="5TH-VB1A">Agent 根据反馈继续执行或者回滚。</li>
</ul>
<p style="color: #191b1f;" data-pid="zkoP7tjz">这不是交互方式的小修小补，这是控制权和复杂性承载位置的转移。</p>
<p style="color: #191b1f;" data-pid="JtbYA5eh">Session First 把复杂性藏在会话里。<br />
Agent First 把复杂性显式地放进系统结构里。</p>
<p style="color: #191b1f;" data-pid="Q0qwK4Ck">我更倾向后者。因为在规模化阶段会 Session First 开始需要大量的修补并且还不合脚。</p>
<h2 style="font-weight: 500; color: #191b1f;">Session First 的红利</h2>
<p style="color: #191b1f;" data-pid="K2rl4doQ">Session First 也不是说不行了，落后了，它只是有自己的舒适区或边界。</p>
<p style="color: #191b1f;" data-pid="Poz6VWWp">它为什么会成为大多数团队的第一个选择？因为它天然适合模型的原生能力。</p>
<p style="color: #191b1f;" data-pid="5bGlRG3o">大模型最成熟的接口就是聊天接口。你给它上下文，它续写；你给它 instruction，它服从；你给它工具定义，它在概率空间里学着调用。这是当前已经成熟了的，并且对于大家的谁知来说没有门槛的。。产品经理能理解，前端能接，后端能包，用户也能马上用。</p>
<p style="color: #191b1f;" data-pid="gm_4ZgMy">从落地顺序看，Session First 有三个明显优势。</p>
<h3 style="font-weight: 500; color: #191b1f;">交付速度快</h3>
<p style="color: #191b1f;" data-pid="hRxCUQ2-">最早一批 AI 应用能起量，基本都吃到了这个红利。做一个对话框，叠一层历史上下文，挂几类工具，外加 prompt 工程和少量业务逻辑，一个可卖的产品就出来了。</p>
<p style="color: #191b1f;" data-pid="z86qAnpw">如果团队处在探索期，需求还没稳定，任务边界也不清晰，Session First 的性价比很高。因为我们可以把大量未定型的业务规则临时编码进 prompt，而不是过早地固化到接口和状态机里。说白了，它适合试错。</p>
<h3 style="font-weight: 500; color: #191b1f;">交互弹性高</h3>
<p style="color: #191b1f;" data-pid="iPte5fGM">很多需求在早期根本说不清。用户自己也不知道该点哪个按钮，只知道「我想把这堆信息处理一下」。这时聊天入口比传统 UI 更自然。Session First 天然适合承接模糊需求，尤其适合开放式任务、咨询类任务、内容类任务。</p>
<h3 style="font-weight: 500; color: #191b1f;">对通识模型友好</h3>
<p style="color: #191b1f;" data-pid="EtKTSjEI">Session First 依赖的是模型在语言理解和上下文整合上的强项。很多场景下，不需要精细建模世界状态，只需要让模型在一个较长的 session 里维持语义连贯，效果就已经够用了。</p>
<p style="color: #191b1f;" data-pid="M-m8vnO9">所以如果一个产品主要解决的是下面这些问题，Session First 我认为完全合理：</p>
<ul style="color: #191b1f;">
<li data-pid="pClrcdBU">单轮或短链路任务；</li>
<li data-pid="u04g5i-D">任务结果主要是文本、建议、草稿、分析；</li>
<li data-pid="EMgZGE3l">工具调用数量少，失败代价低；</li>
<li data-pid="esYI4-QV">用户愿意在回路中持续确认；</li>
<li data-pid="aeA4V2r7">业务状态变化弱，对幂等性和恢复能力要求不高。</li>
</ul>
<p style="color: #191b1f;" data-pid="DQ0eBCYv">比如代码解释、文档问答、简历润色、轻量报表分析、知识库检索助手，这些都很适合。</p>
<p style="color: #191b1f;" data-pid="GG7xR8lI">问题出在很多团队做着做着，把它用到了不该用的地方。</p>
<h2 style="font-weight: 500; color: #191b1f;">Session 的代价</h2>
<p style="color: #191b1f;" data-pid="_IUlwReU">Session First 最大的问题，不是效果问题，而是系统复杂性的位置不对。</p>
<p style="color: #191b1f;" data-pid="b-58aYYB">我们可以把 session 理解成一个不断膨胀的黑盒。任务描述、历史记录、工具调用痕迹、失败重试信息、用户偏好、临时约束，全都塞进去。模型在黑盒里靠注意力机制和 token 预算自行判断什么重要、什么该忽略、什么该继续执行。</p>
<p style="color: #191b1f;" data-pid="35i3DpTY">短任务还行。链路一长，问题就开始多了。</p>
<h3 style="font-weight: 500; color: #191b1f;">状态污染</h3>
<p style="color: #191b1f;" data-pid="WkpeY-5P">会话越长，状态越容易脏。一个典型问题是历史上下文对当前决策的隐性干扰。模型没有传统意义上的干净状态管理，它只有一段被不断续写的上下文。早期的一句错误假设、一次失败调用、一个过时约束，都可能在后续步骤中持续影响行为。</p>
<p style="color: #191b1f;" data-pid="IkL8j6V6">工程上我们会看到一些很烦的现象：</p>
<ul style="color: #191b1f;">
<li data-pid="ypgoyL0L">明明用户已经修改目标，模型还沿着旧计划跑；</li>
<li data-pid="OY4uWoRi">明明工具返回了失败，模型把失败结果当成成功上下文继续推理；</li>
<li data-pid="KVdGdpqP">明明当前任务和上个任务无关，模型还把旧 session 里的偏好带进来。</li>
</ul>
<p style="color: #191b1f;" data-pid="o8pRGOZH">这些都不是 prompt 多写两句能解决的。因为根因在于 session 本身不是一个严谨的状态容器。</p>
<h3 style="font-weight: 500; color: #191b1f;">上下文成本失控</h3>
<p style="color: #191b1f;" data-pid="ISTL4CkN">Session First 很吃上下文。任务越复杂，历史越长，系统 prompt 越复杂，tool spec 越多，token 消耗越惊人。很多团队前期盯着模型单价，后期才发现账单真正膨胀的是「无效上下文」。</p>
<p style="color: #191b1f;" data-pid="AAb4brHY">更糟的是，这部分成本并不总能换来线性收益。超过某个长度以后，模型对上下文的利用率明显下降。你花了更多 token，只换来更模糊的关注分布、更高的遗漏概率。</p>
<p style="color: #191b1f;" data-pid="HaBtPlMi">有一些系统，一次复杂任务真正有价值的工作 token 可能只占总 token 的 20% 到 30%，剩下的都在重复喂历史、喂规则、喂工具描述、喂先前失败记录。这样的系统，成本结构很难看。</p>
<h3 style="font-weight: 500; color: #191b1f;">工具选择不稳定</h3>
<p style="color: #191b1f;" data-pid="g8QZRvno">在 Session First 里，工具往往是通过提示词暴露给模型。模型根据自然语言描述决定什么时候调哪个工具。这种方式灵活，但稳定性一般。尤其当工具数量上来以后，工具间语义重叠、参数边界相近、返回格式不一致，模型的选择质量会迅速下降。</p>
<p style="color: #191b1f;" data-pid="4qfGTgK1">一个常见误区，是以为给工具写更长更详细的 description 就能解决。实际经验正相反：description 越长，竞争工具越多，模型越容易在语义相邻区域摇摆。最终你会发现，问题不是模型笨，而是整个工具层根本没有被设计成适合被模型消费。</p>
<h3 style="font-weight: 500; color: #191b1f;">可恢复性差</h3>
<p style="color: #191b1f;" data-pid="11wrlEFs">Session 是连续流，不擅长离散恢复。任务执行到一半，模型挂了、超时了、工具限流了、用户关闭页面了，系统怎么从中间恢复？很多 Session First 产品的恢复策略要么是重放整个对话，要么让模型读历史「自己想起来」。</p>
<p style="color: #191b1f;" data-pid="lbhAfa1q">这个策略在低风险任务里还能接受，在执行型任务里就很危险。因为它没有明确的检查点，没有确定的已完成步骤，没有结构化的执行日志，恢复质量完全依赖模型在长上下文里的自我理解。</p>
<h3 style="font-weight: 500; color: #191b1f;">可观测性弱</h3>
<p style="color: #191b1f;" data-pid="1BwIcWo5">你问一个 Session First 系统：「为什么它刚才这么做？」答案通常很难给。因为真正的决策过程埋在 session 和模型隐状态里。你最多能看到 prompt、工具调用记录和输出，但很难形成稳定的、可归因的行为分析。</p>
<p style="color: #191b1f;" data-pid="aMr0GfSw">这会直接影响调试、评估、审计和优化。团队很容易陷入一种很熟悉的工作流：改 prompt、跑样例、感觉好一点、上线、再出新问题、继续改 prompt。系统像在「训一头很聪明但脾气不稳定的动物」，而不是在维护一个可控软件。</p>
<h2 style="font-weight: 500; color: #191b1f;">转向 Agent</h2>
<p style="color: #191b1f;" data-pid="_2FvNRl2">Agent First 出现，本质上是在回答一个问题：当任务不再是聊天，而是委托执行时，系统该怎么设计？</p>
<p style="color: #191b1f;" data-pid="FLwpnuCC">用一名话来概括：Session First 优先组织对话，Agent First 优先组织能力、状态和约束。</p>
<p style="color: #191b1f;" data-pid="WCXxazki">这两者的差别，在简单场景里不明显；一旦任务变成多步骤、长周期、高风险、强工具依赖，这个差别会迅速放大。</p>
<p style="color: #191b1f;" data-pid="KpP65M8H">Agent First 的核心变化有三层。</p>
<p style="color: #191b1f;" data-pid="wsX8jMc9">第一层，agent 拥有独立的任务身份。<br />
它不再只是当前聊天窗口里的一个响应函数，而是一个可启动、可暂停、可恢复、可审计的执行单元。它有自己的目标、记忆、工具权限、运行环境和生命周期。</p>
<p style="color: #191b1f;" data-pid="FnF9sNEW">第二层，系统能力被显式结构化。<br />
什么能力能调用，输入输出是什么，失败怎么表示，重试边界在哪，副作用怎么隔离，全部要写清楚。因为 agent 不是靠「猜」来用系统，它得靠规范来用系统。</p>
<p style="color: #191b1f;" data-pid="80wVgyqQ">第三层，任务执行被流程化和可观测化。<br />
agent 的计划、步骤、结果检查、异常处理、人工介入点，都需要成为系统的一部分，而不是 prompt 里的一段希望。</p>
<p style="color: #191b1f;" data-pid="H72jwr7d">这就是为什么我说 Agent First 更像软件设计理念，而不只是交互升级。它要求开发者从一开始就考虑 agent 的接入体验。注意，这里的「接入体验」不是 SDK 文档写得漂不漂亮，而是系统有没有把 agent 当成真正的使用者来对待。</p>
<p style="color: #191b1f;" data-pid="qkJK4dl5">对人友好的系统，重点是 UI。<br />
对 agent 友好的系统，重点是 API、协议、文档、沙箱、日志。</p>
<p style="color: #191b1f;" data-pid="KhUTKVDc">这个顺序一换，整套架构都会变。</p>
<h2 style="font-weight: 500; color: #191b1f;">第一类公民</h2>
<p style="color: #191b1f;" data-pid="U_W5I-Ey">传统软件的第一类公民是人。因为系统操作链条默认由人承担：读页面、理解状态、做选择、点按钮、确认风险、处理异常。</p>
<p style="color: #191b1f;" data-pid="HpE1H8hw">Agent First 的变化在于，这条链路开始迁移给 agent。系统如果还保留「很多关键信息只藏在页面里、很多操作只能靠人脑理解、很多异常只有人看得懂」的设计，那 agent 就只能在外面绕路，最后产品体验会非常拧巴。</p>
<p style="color: #191b1f;" data-pid="nyiOzfI0">所以所谓「agent 是第一类公民」，落到工程上至少意味着四件事。</p>
<h3 style="font-weight: 500; color: #191b1f;">能力必须接口化</h3>
<p style="color: #191b1f;" data-pid="q6mfDNhF">页面点击不是能力，API 才是。<br />
表格展示不是能力，结构化查询和变更才是。<br />
人工读懂的描述不算完成，机器可消费的 schema 才算完成。</p>
<p style="color: #191b1f;" data-pid="IT9qecwX">很多团队表面上说在做 agent，实际上只是让模型去模拟用户点页面，或者让 Playwright 去跑浏览器自动化。这能用，但我通常把它看成过渡手段，不会把它当成长期基建。因为 UI 自动化的脆弱性太高，成本也太高。一旦页面变了、字段换了、弹窗多了、权限策略改了，整个链路就坏了。</p>
<p style="color: #191b1f;" data-pid="z04KhJFd">如果一项业务能力值得被 agent 使用，那它应该先被抽成稳定接口。</p>
<h3 style="font-weight: 500; color: #191b1f;">语义必须外显</h3>
<p style="color: #191b1f;" data-pid="sv-argzO">我们靠经验猜按钮含义，agent 不行。我们得把操作语义、字段含义、约束条件、异常语义都写出来。很多传统系统文档的问题，不是文档少，而是文档默认阅读者是熟悉业务的人。里面有大量省略、上下文跳跃、术语别名、口头约定。</p>
<p style="color: #191b1f;" data-pid="AbUvlfL-">人能脑补。agent 不会脑补，它会误解。</p>
<p style="color: #191b1f;" data-pid="8vz62jG8">机器友好的文档，不是把原文档丢给 RAG 就结束了。它要求内容可索引、可切片、可定位、可引用、可验证。最好还能区分「定义」「约束」「样例」「反例」「危险操作」「返回码语义」。</p>
<h3 style="font-weight: 500; color: #191b1f;">权限必须细粒度</h3>
<p style="color: #191b1f;" data-pid="BiX7vv2-">给人开的权限，往往是按角色开的。给 agent 开权限，粒度要更细。因为 agent 的调用频率高、组合能力强、自动化程度高，任何一个权限放大，都可能把小问题变成系统性事故。</p>
<p style="color: #191b1f;" data-pid="wyE2MMGJ">我见过一些团队初期为了快，直接给 agent 一个「管理员 token」，想着先把链路跑通。跑通确实跑通了，后面风控和审计基本没法收场。Agent First 里，权限模型必须从 day 1 就设计进去。至少要做到工具级、资源级、动作级的边界清晰。</p>
<h3 style="font-weight: 500; color: #191b1f;">结果必须可审计</h3>
<p style="color: #191b1f;" data-pid="5uGts6Js">如果 agent 能执行动作，那所有关键动作都要留痕，谁发起、为什么发起、使用了哪些上下文、调用了哪些工具、拿到了哪些结果、做了什么决策、是否有人确认，都得能追出来。</p>
<p style="color: #191b1f;" data-pid="mGwZ_bFC">这不是为了满足审计部门，而是为了我们自己能把系统维护下去。没有可审计性，复杂 agent 系统很快会变成「偶尔非常惊艳，偶尔完全失控」的黑箱。</p>
<h2 style="font-weight: 500; color: #191b1f;">四层结构</h2>
<p style="color: #191b1f;" data-pid="r--hVVP9">如果我们把 Agent First 的工程原则压缩成一个递进结构，它可以拆成四层：能力层、理解层、连接层、信任层。</p>
<h3 style="font-weight: 500; color: #191b1f;">API 优先</h3>
<p style="color: #191b1f;" data-pid="A4igmFYN">最底下是能力层，也就是 API 优先。它解决的是「Agent 能做什么」。</p>
<p style="color: #191b1f;" data-pid="4JihUE2D">很多 AI 团队容易犯一个错误：先做 prompt，再做工具，再补 API。顺序反了。只要你准备认真做 agent，API 就应该先于 prompt 存在。</p>
<p style="color: #191b1f;" data-pid="2iHv2A0t">因为 prompt 负责引导决策，API 才负责承载能力。没有稳定能力面，agent 的执行质量永远靠运气。</p>
<p style="color: #191b1f;" data-pid="elb7wRm_">我看一个系统适不适合 Agent First，第一眼就看它的 API 长什么样。重点不在 REST 还是 GraphQL，也不在用不用 MCP，而在这几个问题：</p>
<ul style="color: #191b1f;">
<li data-pid="2s6K-9ML">接口语义是否单一清晰；</li>
<li data-pid="ZOsm0FTZ">参数是否结构化且有约束；</li>
<li data-pid="M7y6QpEB">返回值是否可判定成功失败；</li>
<li data-pid="DlzhbZEV">幂等性是否明确；</li>
<li data-pid="cmD_93sl">长任务是否支持异步和回调；</li>
<li data-pid="qBP6SLYm">副作用操作是否支持 dry-run 或预检查；</li>
<li data-pid="PfxPltf2">错误码是否可用于 agent 自恢复。</li>
</ul>
<p style="color: #191b1f;" data-pid="AO9osijd">举个很实际的坑。很多内部系统的 API 是给前端页面写的，不是给 agent 写的。于是你会看到：</p>
<ul style="color: #191b1f;">
<li data-pid="lA_9Gzz2">一个接口返回几十个业务无关字段；</li>
<li data-pid="J3QyM1Sf">失败时只返回「操作失败，请联系管理员」；</li>
<li data-pid="x9A4Ttqr">同一个字段在不同接口里名字还不一样；</li>
<li data-pid="Gu2HXo6H">创建和更新共用一个入口，副作用混杂；</li>
<li data-pid="jQpa3MR4">数据查询支持模糊匹配，但没有稳定过滤条件。</li>
</ul>
<p style="color: #191b1f;" data-pid="8sN_fbkf">这种 API 给前端开发凑合能用，给 agent 基本就是灾难。因为 agent 消费接口时最怕三件事：语义不稳定、返回不可判定、失败不可恢复。</p>
<p style="color: #191b1f;" data-pid="z6FsivtG">API 优先不是一句口号，它意味着你要为了 agent 重写一部分服务边界。代价不小，但省下的是后面无数轮 prompt 补丁。</p>
<h3 style="font-weight: 500; color: #191b1f;">机器文档</h3>
<p style="color: #191b1f;" data-pid="y-sS3fH3">有了能力层，还不够。Agent 知道系统「能做什么」，不代表它知道「怎么正确地做」。</p>
<p style="color: #191b1f;" data-pid="tIfhwniQ">这就是理解层，也就是机器友好文档。</p>
<p style="color: #191b1f;" data-pid="5Yrlt4GA">很多团队对文档的理解还停留在「给模型塞进知识库」。坦白说，这一步最多算资料接入，不算文档工程。机器友好文档要求内容本身就是为 agent 理解和执行设计的。</p>
<p style="color: #191b1f;" data-pid="ZRf9ulQZ">我们写文档喜欢写背景、写故事、写注意事项穿插在长段落里。机器文档要反过来，尽量消除叙事性，强化检索和判定性。什么场景能用哪个接口，前置条件是什么，参数组合有什么限制，成功条件是什么，失败后应该重试还是终止，全部要能被定位出来。</p>
<p style="color: #191b1f;" data-pid="oECaLAXY">我比较推崇一种写法：把文档拆成五类最小单元。</p>
<ol style="color: #191b1f;">
<li data-pid="MW6izXr4">定义单元：术语、对象、字段、状态含义。</li>
<li data-pid="PESSY8gQ">操作单元：动作描述、输入输出、前置条件、后置条件。</li>
<li data-pid="7Dp5JAcB">约束单元：权限要求、配额、时序、互斥关系。</li>
<li data-pid="Okrzq469">异常单元：错误码、成因、恢复建议、是否可重试。</li>
<li data-pid="aAcF6hWc">样例单元：正确示例、错误示例、边界示例。</li>
</ol>
<p style="color: #191b1f;" data-pid="TowYNEzN">这样写出来的文档，对人读可能不友好，但对 agent 非常友好。因为 agent 需要的不是阅读体验，而是最短路径上的高密度语义。</p>
<p style="color: #191b1f;" data-pid="DWHac5Lh">还有一个容易被忽略的点：文档版本化。<br />
如果系统能力在变，而 agent 依赖旧文档决策，事故几乎是必然的。机器文档必须带版本、带生效范围、带弃用说明。更进一步，文档变更最好能够被 agent 订阅或者被平台主动推送。否则你会得到一堆「以前能跑今天突然不行」的鬼问题。</p>
<h3 style="font-weight: 500; color: #191b1f;">协议层</h3>
<p style="color: #191b1f;" data-pid="2vQVOH_W">再往上一层是连接层，也就是标准化协议。我们都知道 MCP，它当前重要性确实在上升。</p>
<p style="color: #191b1f;" data-pid="w7t9LhTp">Agent First 一旦进入平台化阶段，连接成本会成为瓶颈。每接一个系统都重新对工具做 schema 包装、鉴权对接、错误语义映射、流式交互适配，团队很快就会被集成工作拖死。标准协议的价值就在这里：让 agent 对外部能力做到尽可能低摩擦的即插即用。</p>
<p style="color: #191b1f;" data-pid="5BuRoxJ-">协议不是为了优雅，是为了降低耦合和重复劳动。它至少解决三个问题：</p>
<ul style="color: #191b1f;">
<li data-pid="42aDrj4i">能力发现：agent 如何知道外部提供了哪些工具和资源；</li>
<li data-pid="mWEcxg_n">调用协商：参数 schema、返回 schema、流式能力、状态反馈如何统一；</li>
<li data-pid="qBc-jFf5">安全边界：认证、授权、调用隔离、资源限制如何标准化表达。</li>
</ul>
<p style="color: #191b1f;" data-pid="Io6gZEhK">MCP 这类协议的意义，不只是统一 tool calling 的表面格式，更重要的是把「能力元数据」变成平台可理解的对象。一旦元数据标准化，很多平台能力才能长出来：自动装配、权限编排、调用治理、能力市场、兼容性检查、离线评测。</p>
<p style="color: #191b1f;" data-pid="UD9KwzhD">协议统一不会自动带来效果统一。现实里最常见的问题是：大家都说自己支持标准，结果 schema 质量参差不齐，语义粒度不一致，错误处理风格也不同。最后 agent 虽然能连上，依旧很难稳定用好。</p>
<p style="color: #191b1f;" data-pid="E3V0zbBN">所以协议层只是接入下限，不是体验上限。系统方如果指望「支持 MCP 了，agent 就能用了」，十有八九会失望。</p>
<h3 style="font-weight: 500; color: #191b1f;">信任成本</h3>
<p style="color: #191b1f;" data-pid="RfQwCW9C">最上面一层是信任层：安全、沙箱、可观测性。它解决的是「Agent 如何被安全地使用」。</p>
<p style="color: #191b1f;" data-pid="z4X0EalX">这层往往是被低估的。因为很多团队在前期更关心效果演示，安全和观测总想放后面补。等 agent 真开始执行动作，补起来就很痛苦。</p>
<p style="color: #191b1f;" data-pid="EfUj4D2a">Agent 系统的风险和传统自动化脚本不完全一样。脚本通常路径固定、输入有限、行为可枚举。Agent 的输入是开放的，计划是动态的，工具组合是可变的，自修正带来恢复能力，也带来行为不可预测性。这种系统如果没有信任层，部署规模一上来迟早出事。</p>
<p style="color: #191b1f;" data-pid="Rq3kFvBu">信任层可以分为三块。</p>
<h3 style="font-weight: 500; color: #191b1f;">安全边界</h3>
<p style="color: #191b1f;" data-pid="hzeX45vF">包括权限控制、数据隔离、密钥管理、配额限制、危险操作确认机制。所有高风险动作都要能分级：只读、可写、可执行、不可逆。不同级别的动作，对应不同的确认和审计策略。</p>
<p style="color: #191b1f;" data-pid="sys4IsBL">还有一件事必须单独说：prompt injection。<br />
只要 agent 会读取外部内容、再基于内容调用工具，prompt injection 就不是附加风险，而是基本风险。你不能假设模型会自己免疫。系统层必须有输入隔离、指令优先级控制、工具调用白名单、敏感动作二次确认、结果验证。</p>
<h3 style="font-weight: 500; color: #191b1f;">沙箱执行</h3>
<p style="color: #191b1f;" data-pid="1qt69UP0">如果 agent 能运行代码、操作文件、访问网络，就必须有沙箱。别指望靠「模型会守规矩」来兜底。资源限制、网络出口策略、文件系统隔离、进程生命周期管理，这些都是必须项。</p>
<p style="color: #191b1f;" data-pid="P0jKIeFS">很多桌面端产品今天还在用一个比较重的本地 session 容器去承接 agent 行为，这在早期合理，因为本地环境天然带着用户上下文和工具可得性。但一旦平台化，执行环境一定要可控、可回收、可复制。否则你会发现 bug 根本没法稳定复现。</p>
<h3 style="font-weight: 500; color: #191b1f;">可观测性</h3>
<p style="color: #191b1f;" data-pid="Pl7zc4zu">这一块经常被做得太浅。普通日志不够。你需要的是面向 agent 的运行时观测：任务级 trace、步骤级事件、工具调用链、上下文快照、计划变更、重试原因、人工接管点、最终结果评估。</p>
<p style="color: #191b1f;" data-pid="_sQQ-OmB">有了这些数据，很多事情才有可能做：行为分析、失败归因、离线回放、A/B 对比、策略优化、合规审计。</p>
<p style="color: #191b1f;" data-pid="VlCSTrO4">我甚至会说，没有可观测性，Agent First 根本不成立。因为你没法持续优化一个你看不见的系统。</p>
<h3 style="font-weight: 500; color: #191b1f;">单体与团队</h3>
<p style="color: #191b1f;" data-pid="KbTr2nmc">再往前一步，Agent First 还会带来一个自然演进：从单一 agent 走向 agent teams。</p>
<p style="color: #191b1f;" data-pid="F3isRc-L">这个趋势不难理解。单体 agent 的优势是简单、上下文统一、决策路径短。缺点也明显：任务一复杂，规划、执行、验证、知识检索、异常恢复全压在一个主体上，容易出现上下文拥堵和角色冲突。模型一边要想战略，一边要写细节，一边还要检查自己，稳定性会下降。</p>
<p style="color: #191b1f;" data-pid="K88LyqEo">多 agent 协作的思路，是把不同职责拆开。比如规划 agent 负责分解任务，执行 agent 负责调用工具，验证 agent 负责检查输出，审计 agent 负责看风险和合规。你提到 Sakana AI，我觉得它给行业的启发就在这里：复杂问题的效果上限，未必来自更大的单体，而可能来自更合理的协作结构。</p>
<p style="color: #191b1f;" data-pid="_9PvI3l1">但别高估 agent teams 的短期收益。它的工程成本很高。</p>
<p style="color: #191b1f;" data-pid="GBQ5As-1">第一，通信成本会增加。<br />
agent 之间传递的信息如果不够压缩，token 成本会迅速膨胀。很多团队做多 agent，最后账单翻倍，效果提升却不明显，问题就出在这里。</p>
<p style="color: #191b1f;" data-pid="k2LlSq6C">第二，错误归因更难。<br />
单体 agent 出错，你还知道看一条链。多 agent 出错，很可能是上游规划有偏差、中游执行误解了任务、下游验证规则又太松。没有细致的 trace，很难定位。</p>
<p style="color: #191b1f;" data-pid="XSuoPjJN">第三，协作协议本身就是复杂度。<br />
谁能给谁发任务，谁对谁有覆盖权，冲突怎么解决，结果以谁为准，失败是否回滚，人工在什么点介入，这些全得定规则。规则一多，系统会变得很重。</p>
<p style="color: #191b1f;" data-pid="sEiM2vWO">它是复杂任务的方向，但不是所有产品都该急着上。很多团队连单体 agent 的能力边界、观测体系、权限模型都没建好，就直接跳多 agent，最后只会把问题放大。</p>
<h2 style="font-weight: 500; color: #191b1f;">个人的判断</h2>
<p style="color: #191b1f;" data-pid="PygqIP5D">如果今天让我给团队定路线，我不会在所有场景里一刀切推 Agent First，也不会继续把 Session First 当主架构。</p>
<p style="color: #191b1f;" data-pid="MBtLFSYJ">我的判断是这样的：</p>
<p style="color: #191b1f;" data-pid="pJoRgTgx">凡是以「理解、生成、陪伴、咨询」为主，任务链条短，用户始终在线，副作用低的场景，Session First 依然是最有效率的方案。别把简单问题搞复杂。一个高质量 session 产品，照样能有非常强的竞争力。</p>
<p style="color: #191b1f;" data-pid="j575XQI9">凡是以「委托执行、跨系统操作、长周期任务、可恢复流程、强审计要求」为主的场景，应该尽早转向 Agent First。拖得越久，后面迁移成本越高。因为你的 prompt、工具、日志、权限、数据结构都会按 Session First 的惯性越长越歪，最后很难矫正。</p>
<p style="color: #191b1f;" data-pid="3EZ1NUZd">从行业演进看，我认为我们会经历三个阶段：</p>
<p style="color: #191b1f;" data-pid="0YVja-AL">第一阶段，通识模型 + 聊天入口主导。<br />
第二阶段，聊天入口还在，但底层逐步 agent 化，能力和状态开始结构化。<br />
第三阶段，很多系统默认面向 agent 开放，我们反而通过 agent 间接使用软件。</p>
<p style="color: #191b1f;" data-pid="r5BSjN4m">今天大多数团队还处在第一阶段和第二阶段之间。很多产品表面上已经在说 agent，骨子里还是 session 产品。这个阶段没什么丢人的，行业本来就处在过渡期。问题在于，团队自己得知道自己在哪，不要把一个 prompt-heavy 的聊天系统误以为已经完成了架构升级。</p>
<h2 style="font-weight: 500; color: #191b1f;">小结</h2>
<p style="color: #191b1f;" data-pid="z5MFKqKb">Session First 帮行业把 AI 快速带进了产品。它的重要性不用否认。没有这一阶段，大量需求不会被激活，很多团队也不会积累起对模型能力边界的真实理解。</p>
<p style="color: #191b1f;" data-pid="WS_S9UCb">但它的问题也越来越明显：状态管理松散、成本结构失真、工具使用脆弱、恢复和审计能力薄弱。任务越复杂，缺点越放大。</p>
<p style="color: #191b1f;" data-pid="3iI6p0Tu">Agent First 把软件重新组织了一遍：把会话里的隐含复杂性，搬回系统里显式管理；把面向人的操作界面，扩展成面向 agent 的能力界面；把「模型偶尔能做成」变成「系统可稳定交付」。</p>
<p style="color: #191b1f;" data-pid="PRw02bJa">如果要走这条路，我们就需要重写 API，补文档债，需要重做权限和日志，就会发现以前很多偷过的懒都要补回来。可如果我们的目标不是做一个会聊天的功能，而是做一个能被委托工作的系统，这些账早晚都要还。</p>
<p style="color: #191b1f;" data-pid="Of7Yt7UJ">所以我现在看一个 AI 产品会更关心它底下埋的是 session，还是 agent。前者决定它今天看起来多聪明，后者决定它一年后还能不能继续长。</p>
<p style="color: #191b1f;" data-pid="1UCPsXHI">以上。</p>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/07/session-first-and-agent-first/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>聊聊 Harness：从 Agent 到组织</title>
		<link>https://www.phppan.com/2026/05/harness-engineering-anent-org/</link>
		<comments>https://www.phppan.com/2026/05/harness-engineering-anent-org/#comments</comments>
		<pubDate>Sat, 30 May 2026 09:35:05 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[harness]]></category>
		<category><![CDATA[harness engineering]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2501</guid>
		<description><![CDATA[我们在落地 Agent 时面临的核心矛盾，是大模型的概率生成机制与工程系统所需的绝对确定性存在天然冲突。要获取 [&#8230;]]]></description>
				<content:encoded><![CDATA[<section id="nice" style="color: #000000;" data-tool="mdnice编辑器" data-website="https://www.mdnice.com">
<p data-tool="mdnice编辑器">我们在落地 Agent 时面临的核心矛盾，是大模型的概率生成机制与工程系统所需的绝对确定性存在天然冲突。要获取大规模、可维护且值得信赖的代码，必须在系统外围构建 Harness。<strong>Harness 的本质是将不确定性转化为确定性。</strong></p>
<p data-tool="mdnice编辑器">提高信任度和可靠性需要极度压缩 Agent 的解决方案空间。我们必须放弃让模型「生成任何内容」的灵活性，转而采用包含大量技术细节的提示、规则和框架。特定的架构模式、强制执行的边界以及标准化的结构，构成了这套护栏的物理基础。</p>
<p data-tool="mdnice编辑器">当前越来越多的团队在持续快速的产生代码，而这些演示很好看，当真的进入整个软件生命周期中，就会产生混乱，当越来越多的人随着时间的推移在仓库中堆砌代码，组织就开始堆人进行 review、反复返工，最后 AI 的吞吐量被人类注意力卡死，表面上用了 Agent，实际产能没上去，维护成本还更高。</p>
<p data-tool="mdnice编辑器">当然，这是一种结果，也有人在过程中不停的构建基建，做 Harness 工程，整个代码不再是无序的扩张。从这个逻辑来讲，harness 的作用是把<strong>大模型输出从概率事件压回工程确定性的系统设计</strong>。</p>
<h1 data-tool="mdnice编辑器"><span class="content">Agent 的 Harness</span></h1>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #ffffff;">harness 是什么</span></h2>
<p data-tool="mdnice编辑器"><strong>很多人把 harness 比作一个操作系统，模型是 CPU，但我觉得并不是。</strong></p>
<p data-tool="mdnice编辑器">如果模型真的是 CPU，那它接收指令后的执行结果应该是绝对严格且可预测的；但大模型本质上是一个概率引擎，它在潜空间里做的是模式匹配与概率生成。因此，harness 并不是像操作系统那样去调度底层硬件资源或分配内存，它更像是一套概率过滤器和对齐机制。它依靠纯粹的工程手段，把模型那种发散的、充满不确定性的「创造力」或「幻觉」，强行压缩进一条狭窄、严谨且符合人类预期的流水线里。</p>
<p data-tool="mdnice编辑器">这种工程逻辑在实践中，体现为无处不在的防御性设计和反馈闭环。当模型吐出一串代码或一个决策时，harness 并不负责直接「运行」它，而是负责「质检」和「纠偏」。它通过静态检查、架构规则扫描、自动化测试和沙箱验证，把模型给出的「大概率正确」转化为工程上非黑即白的「通过或驳回」。正是这种让概率不断撞击确定性规则的过程，才使得最终沉淀到代码库里的产物是安全、可控且符合系统长期利益的。</p>
<p data-tool="mdnice编辑器">harness 解决确定性问题的终极目的，是为了在系统中建立无需人工干预的信任，从而真正释放 AI 的吞吐量。如果没有这套逻辑，模型生成的代码越多，人类审查的负担就越重，整个组织的运转速度依然会被人类的注意力瓶颈卡死。</p>
<p data-tool="mdnice编辑器">Martin Fowler 的博客中发表了 Thoughtworks 的技术专家的一篇文章，将 OpenAI 文章中所描述的 harness 分为三个方面：</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #ffffff;">上下文工程</span></h2>
<p data-tool="mdnice编辑器">上下文工程需要做到动态与静态的交织</p>
<p data-tool="mdnice编辑器">单纯依赖超长 Prompt 无法解决复杂工程问题。上下文工程的核心在于构建代码库中持续增强的知识库，并打通 Agent 对动态上下文的访问路径。</p>
<p data-tool="mdnice编辑器">静态知识库定义了系统的基础法则。我们将领域模型、API 契约和历史架构决策文档化，作为 Agent 初始化的基线上下文。动态上下文决定了 Agent 在运行时的决策质量。系统需要将实时的可观测性数据、测试覆盖率报告甚至浏览器导航状态，实时注入到 Agent 的工作流中。缺乏动态上下文的 Agent 就像蒙眼狂奔的打字机，产出的代码在语法上完美，在逻辑上完全脱离系统现状。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #ffffff;">架构约束</span></h2>
<p data-tool="mdnice编辑器">架构约束是确定性的防线。</p>
<p data-tool="mdnice编辑器">完全依赖 LLM 进行自我反思和代码审查，在生产环境中极度危险。架构约束必须由确定性的自定义代码检查器和结构测试来强制执行。</p>
<p data-tool="mdnice编辑器">我们通过静态分析工具拦截不合规的依赖调用，利用 AST（抽象语法树）解析确保代码分层符合规范。当 Agent 试图在 UI 层直接发起数据库连接时，确定性的检查器会立即阻断该行为，并将具体的错误堆栈和修复路径作为反馈输入给 Agent。这种混合架构确保了系统的底线由死板的规则守卫，Agent 的创造力被严格限制在安全的沙盒内。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #ffffff;">垃圾回收</span></h2>
<p data-tool="mdnice编辑器">垃圾回收主要是用于对抗代码熵增。</p>
<p data-tool="mdnice编辑器">完全自主的智能体引入了代码库衰败的新问题。Agent 会精准且不知疲倦地复现代码仓库中已存在的模式，包含那些不均衡或不够理想的遗留设计。随着时间的推移，这种行为不可避免地导致系统架构漂移。</p>
<p data-tool="mdnice编辑器">最初，人类开发者试图手动处理这个问题。团队过去每周五要花费 20% 的时间清理「AI 残渣」。这种依赖人力的做法毫无可扩展性。</p>
<p data-tool="mdnice编辑器">我们将资深工程师的主观品味转化为机械规则，提炼为「黄金原则」并直接编码到代码仓库中，建立了一个循环清理流程。我们强制要求使用共享的实用程序包，禁止手工编写零散的辅助工具，确保不变式集中管理。我们严禁使用猜测性的数据探测，强制验证边界或依赖类型化的 SDK，防止 Agent 基于虚幻的结构进行构建。</p>
<p data-tool="mdnice编辑器">系统定期运行一组后台 Agent 任务，扫描代码库中的偏差、更新质量等级，并发起有针对性的重构 Pull Request。这些 PR 大多可以在一分钟内完成审查并自动合并。这套机制的功能等同于内存管理中的垃圾回收。技术债务如同高息贷款，通过高频的微小重构不断偿还，远胜过让债务累积到系统崩溃。人类的架构品味一旦被捕获并规则化，就会无情地应用于每一行代码，每天自动发现并消灭不良模式。</p>
<h1 data-tool="mdnice编辑器"><span class="content">AI Agent Harness 的工程化落地</span></h1>
<p data-tool="mdnice编辑器">从几个流行的框架来看，主要是从流程强化、规格沉淀、任务编排等逻辑上来做事情。</p>
<p data-tool="mdnice编辑器">将这些逻辑拆开可以分为四个维度：</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #ffffff;">上下文工程</span></h2>
<p data-tool="mdnice编辑器">上下文工程主要是在规范层解决问题，其主要解决的「规则文件失控」的问题，实现规格沉淀与对齐，以及上下文工程的可控。</p>
<p data-tool="mdnice编辑器">之前，我们习惯把所有规范塞进类似于单个 <code style="color: #ef7060;">.cursorrules</code> 文件，导致 AI 上下文过载且容易忽略细节。这一层落地的第一步是建立结构化、按需加载的规范体系。主要做到如下的点：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;"><strong style="color: #000000;">规范模块化</strong>：将系统架构、数据库规范、错误处理等拆分为独立的结构化文档。</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #000000;">按需检索</strong>：AI 不需要每次都通读所有规范，而是根据当前所处的任务阶段，动态检索并加载所需的上下文。</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #000000;">任务记忆隔离</strong>：为每个独立任务建立物理隔离的工作区和日志。AI 每次开启新会话时，只读取当前任务的精确记忆，既解决了“跨会话失忆”，又屏蔽了无关信息的干扰。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">以 Trellis 框架为例，Trellis 摒弃了单一庞大的全局提示词文件，而是采用 spec/ 目录将规范模块化（如拆分为 database-guidelines.md）。在执行任务时，它利用 tasks/ 目录下的 JSONL 配置文件，让 Agent 动态检索并按需加载上下文。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #ffffff;">架构约束</span></h2>
<p data-tool="mdnice编辑器">架构约束的<strong>核心逻辑：用代码约束代码，实现闭环自愈。</strong> 口头约定或纯文本规范在 AI 面前是脆弱的，它极易为了「跑通逻辑」而破坏架构分层。</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;"><strong style="color: #000000;">规则代码化</strong>：将核心的架构依赖规则（例如“前端组件严禁直接调用数据库”）编写为静态分析脚本或自定义 Linter。</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #000000;">带解释的强阻断</strong>：在代码提交或验证阶段强制执行这些拦截器。关键在于，报错信息不能仅仅是「检查失败」，必须输出高度结构化的指导：明确告诉 AI“为什么违反了规则”以及“正确的做法是什么”。</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #000000;">自动修复</strong>：AI 读取到结构化的报错指导后，能够自动理解并修正代码，形成无需人类介入的自愈闭环。</section>
</li>
</ul>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #ffffff;">反馈循环</span></h2>
<p data-tool="mdnice编辑器"><strong>核心逻辑：降噪处理，防范死循环。</strong> LLM 的注意力会被长篇大论的日志（如几千行的覆盖率输出）稀释注意力，从而忽略真正致命的错误。 因此我们需要做到：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;"><strong style="color: #000000;">零输出原则</strong>：改造验证脚本。如果测试通过，脚本应保持完全沉默；如果失败，只输出精简的错误堆栈和失败原因。</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #000000;">强制验收清单</strong>：在 AI 试图标记任务「已完成」之前，系统应强制拦截，要求其对照需求文档逐项确认边界条件。</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #000000;">防死循环干预</strong>：设定重试阈值。如果 AI 对同一文件连续修改多次且测试依然失败，系统应主动中断并强制其回滚代码、重新审视需求，防止 AI 陷入无效的「幻觉修 Bug」循环。</section>
</li>
</ul>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #ffffff;">熵管理</span></h2>
<p data-tool="mdnice编辑器">熵管理主要是阻断「坏模式」的指数级扩散</p>
<p data-tool="mdnice编辑器"><strong>核心逻辑：快速偿还技术债。</strong> AI 复制坏代码的速度是指数级的。一旦允许一个临时的妥协方案合入主分支，AI 会在极短时间内将其复制到整个代码库。</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;"><strong style="color: #000000;">高频垃圾收集</strong>：彻底放弃“集中清技术债”的传统做法。每天必须安排固定时间，专门 Review AI 生成的代码（人工或 AI 自动），及时识别新引入的坏模式。</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #000000;">规范资产的动态演进</strong>：一旦发现坏模式，立即让 AI 深度分析根因，并<strong style="color: #000000;">自动将正确的防范规则更新到规范库中</strong>。</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #000000;">团队级免疫</strong>：由于规范库与代码同源管理（存在于 Git 仓库中），当这段新规则被提交后，团队其他成员拉取代码时，他们的 AI 助手就能立刻“学会”这个新技能。这把偿还技术债的动作，变成了每天自动化、可积累的系统进化。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">以 Cursor 为例，可以更新 Team Rules</p>
<h1 data-tool="mdnice编辑器"><span class="content">组织级 Harness</span></h1>
<p data-tool="mdnice编辑器">聊完 Agent 的 Harness，再聊一下组织的。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #ffffff;">人的角色已经变了</span></h2>
<p data-tool="mdnice编辑器">大家都知道康威定律，简单来说就是：<strong>设计系统的组织，其产生的设计受限于这些组织的沟通结构。</strong></p>
<p data-tool="mdnice编辑器">而系统设计到最后，也一定会遇到一个问题：<strong>谁来定义规则，谁来解释例外，谁来承担后果。</strong></p>
<p data-tool="mdnice编辑器">以前的软件开发分工相对稳定。PM 写需求，设计出稿，前后端分别实现，测试验证，运维发布。大家各自占一段链路，边界虽然有摩擦，但总体清楚。</p>
<p data-tool="mdnice编辑器">AI 进来以后，边界开始模糊。</p>
<p data-tool="mdnice编辑器">PM 已经可以直接产出前端原型，很多时候产出的还不是静态图，而是真能跑的页面代码。设计师也不再只是给稿子，很多交互和组件约束可以直接沉淀成生成资产。前端工程师花在纯页面搭建上的时间下降，开始更多介入状态管理、交互抽象、可维护性收拢。后端和算法也更早被拉进来，因为很多 AI 生成的原型一开始就会碰到真实数据和能力边界。</p>
<p data-tool="mdnice编辑器">这是现在很多团队正在进行的转型。</p>
<p data-tool="mdnice编辑器">如果组织还按旧的分工运转，Agent 会把协作缝隙快速放大。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #ffffff;">组织级 Harness 要管什么</span></h2>
<p data-tool="mdnice编辑器">我理解的组织级 harness，重点在三件事：</p>
<ol data-tool="mdnice编辑器">
<li>
<section style="color: #010101;"><strong style="color: #000000;">定义新的协作接口</strong></section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #000000;">重新分配注意力</strong></section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #000000;">把责任从「谁写了代码」改成「谁定义了系统」</strong></section>
</li>
</ol>
<h3 data-tool="mdnice编辑器"><span class="content">协作接口要前移</span></h3>
<p data-tool="mdnice编辑器">以前很多问题可以留到开发阶段再对齐。现在不行。</p>
<p data-tool="mdnice编辑器">因为 PM 通过 AI 已经能直接产出前端代码，需求不再是文字说明，而可能是一个可交互原型；设计规范也不再只是 Figma 标注，而是可以半自动映射到组件约束；后端接口能力如果不提前讲清楚，前面的生成很容易一路偏到错误方向。</p>
<p data-tool="mdnice编辑器">所以组织里的评审必须前移，重点也得改。</p>
<p data-tool="mdnice编辑器">过去的需求评审，很多时候在讨论功能要不要做。现在要多讨论三件事：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;">验收标准到底是什么</section>
</li>
<li>
<section style="color: #010101;">哪些边界不能突破</section>
</li>
<li>
<section style="color: #010101;">哪些部分允许先用原型推进，哪些必须工程化收拢后才能上线</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这几个东西不提前定，后面会出现一个很常见的问题：原型阶段看起来进展飞快，进入工程化后才发现返工巨大。</p>
<h3 data-tool="mdnice编辑器"><span class="content">注意力要重新分配</span></h3>
<p data-tool="mdnice编辑器">我现在越来越少鼓励资深工程师花时间逐行抠低风险代码。</p>
<p data-tool="mdnice编辑器">这不是说 review 不重要，而是注意力要贵着用。</p>
<p data-tool="mdnice编辑器">在 Agent 环境里，重要的工作变成了：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;">定义验收标准</section>
</li>
<li>
<section style="color: #010101;">设计架构边界</section>
</li>
<li>
<section style="color: #010101;">提炼黄金原则</section>
</li>
<li>
<section style="color: #010101;">识别系统性失败信号</section>
</li>
<li>
<section style="color: #010101;">决定哪些异常值得阻塞主流程</section>
</li>
<li>
<section style="color: #010101;">审核高风险改动和高影响面重构</section>
</li>
</ul>
<p data-tool="mdnice编辑器">反过来，低风险、重复性、局部性的东西，应该尽量交给自动化校验和后台清理任务。</p>
<p data-tool="mdnice编辑器">如果一个组织还在让最贵的人力去看大批格式化差异、小工具改名、重复样板代码，那 harness 基本等于没有。</p>
<h3 data-tool="mdnice编辑器"><span class="content">责任归属要重写</span></h3>
<p data-tool="mdnice编辑器"><strong>在 AI-Native 组织里，谁对结果负责？</strong></p>
<p data-tool="mdnice编辑器">PM 产出了页面代码，前端做了工程化收拢，Agent 自动补了测试，清理 Agent 又改了一轮共享工具。最后线上出问题，算谁的？</p>
<p data-tool="mdnice编辑器">如果这个问题没有明确答案，团队会很快进入防御状态。每个人都怕接 AI 产出的锅，于是流程开始重新变重，所有人都试图把责任往后传。</p>
<p data-tool="mdnice编辑器">所以组织级 harness 一定要明确责任模型。</p>
<p data-tool="mdnice编辑器">可以按三层分：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;"><strong style="color: #000000;">需求责任</strong>：谁定义了目标与验收标准，谁负责需求正确性</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #000000;">架构责任</strong>：谁定义了边界、模式和约束，谁负责系统一致性</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #000000;">发布责任</strong>：谁决定进入生产环境，谁负责风险接受</section>
</li>
</ul>
<p data-tool="mdnice编辑器">不要再执着于「谁手写了这行代码」。就像团队管理一样，最后拍板的人担责。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #ffffff;">可落地的 AI-Native 研发流程</span></h2>
<p data-tool="mdnice编辑器">以下为我们当前在跑的流程：</p>
<h3 data-tool="mdnice编辑器"><span class="content">需求生成</span></h3>
<p data-tool="mdnice编辑器">第一步由 PM 主导，但交付物不再只是 PRD，而是<strong>带验收标准的可运行原型</strong>。</p>
<p data-tool="mdnice编辑器">但是，原型代码不等于可直接上线代码。它的价值是澄清需求、暴露分歧、提前感知交互复杂度。</p>
<p data-tool="mdnice编辑器">所以 PM 可以生成，但不能默认拥有工程决策权。最终所有的代码都需要前端工程师构建的工具链条，以及 AI 和人工的审核及合入。</p>
<h3 data-tool="mdnice编辑器"><span class="content">联合评审</span></h3>
<p data-tool="mdnice编辑器">第二步是全员参与的需求评审与架构设计。设计、前端、后端、算法都要尽早介入。</p>
<p data-tool="mdnice编辑器">这个阶段重点不是抠实现细节，而是确定：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;">用户路径是否成立</section>
</li>
<li>
<section style="color: #010101;">数据流怎么走</section>
</li>
<li>
<section style="color: #010101;">状态边界怎么划</section>
</li>
<li>
<section style="color: #010101;">哪些能力用现有服务承接</section>
</li>
<li>
<section style="color: #010101;">哪些模块需要新增抽象</section>
</li>
<li>
<section style="color: #010101;">风险点在哪</section>
</li>
<li>
<section style="color: #010101;">验收怎么自动化</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这这一步的产出结构化进仓库，因为后面它会直接成为 Agent 的约束输入。</p>
<h3 data-tool="mdnice编辑器"><span class="content">工程收拢</span></h3>
<p data-tool="mdnice编辑器">第三步是工程化整合。这个阶段前端、后端、算法开始把前面的原型和需求收敛进正式系统。</p>
<p data-tool="mdnice编辑器">这里 Agent 会大量参与，但人类不能退出。重点工作包括：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;">把原型重构进现有组件和模块体系</section>
</li>
<li>
<section style="color: #010101;">校正状态管理、错误处理、埋点、权限、监控</section>
</li>
<li>
<section style="color: #010101;">对接真实接口和算法能力</section>
</li>
<li>
<section style="color: #010101;">补齐类型、边界验证和回归测试</section>
</li>
<li>
<section style="color: #010101;">处理跨模块影响面</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这一段最考验 harness，因为原型代码最容易带着局部最优、全局失真、风格漂移的问题冲进主仓。</p>
<h3 data-tool="mdnice编辑器"><span class="content">自动验证与灰度</span></h3>
<p data-tool="mdnice编辑器">最后一步是自动化测试、灰度发布和反馈回收。</p>
<p data-tool="mdnice编辑器">这一步先由专门的工程团队来负责，加入部分的 AI 成分，固化系统。</p>
<h1 data-tool="mdnice编辑器"><span class="content">从 Agent 到组织，真正难的是控制系统</span></h1>
<p data-tool="mdnice编辑器">很多人以为 AI 落地的核心挑战在模型能力、成本或者工具接入。我现在看，最大挑战更集中在三个词：<strong>环境、反馈回路、控制系统。</strong></p>
<p data-tool="mdnice编辑器">环境决定 Agent 看到了什么、能做什么、不能做什么。</p>
<p data-tool="mdnice编辑器">反馈回路决定错误会被放大，还是会被系统吸收成改进信号。</p>
<p data-tool="mdnice编辑器">控制系统决定生成能力增长之后，组织是变得更稳，还是更乱。</p>
<p data-tool="mdnice编辑器">这三个东西做不好，模型再强也只是更快地产生问题。</p>
<p data-tool="mdnice编辑器">做得好，哪怕模型能力没到最顶尖，系统一样能稳定进化。因为工程上真正稀缺的，从来不是一次惊艳输出，而是长期重复地产出靠谱结果。</p>
<h1 data-tool="mdnice编辑器"><span class="content">组织的 AI-Native 化</span></h1>
<p data-tool="mdnice编辑器">组织的 AI-Native 化也是慢慢进货，逐步推进的，先从小范围试起，再根据结果不断调整规则和流程。并且各家有各家的风格和气质。</p>
<p data-tool="mdnice编辑器">第一，<strong>选一条链路打透</strong>。不要一开始就全组织铺开。先找一个协作关系清楚、反馈周期短、风险相对可控的场景，比如中后台、运营工具、内部系统，或者低风险服务改造。重点不是让 AI 多写代码，而是先验证：信息怎么给、边界怎么定、错误怎么发现、问题怎么清理。</p>
<p data-tool="mdnice编辑器">第二，<strong>先改规则，再谈效率</strong>。很多团队一上来就问产能能提升多少，但更重要的是：规则有没有沉淀下来，错误能不能回流，坏模式能不能及时发现并清掉。如果这些没做好，所谓提效往往只是把问题推后，甚至把混乱放大。</p>
<p data-tool="mdnice编辑器">第三，<strong>把人的位置往上移</strong>。资深工程师要逐渐从大量写代码，转向定规则、画边界、看反馈；技术管理者要从盯人和排期，转向设计流程、分层风险、明确责任；产品可以更早参与原型，但不能越过工程判断。</p>
<p data-tool="mdnice编辑器">组织真正变成 AI-Native，不是因为每个人都在用 Agent，而是协作方式已经围绕 Agent 被重新设计过。</p>
<p data-tool="mdnice编辑器">模型当然重要，但不是决定性因素。真正拉开差距的，是谁先意识到：Agent 不是一个更快的开发者，而是一个高吞吐的生产单元。它会放大环境本身。规则清楚，它就放大规则；流程混乱，它就放大混乱。</p>
<p data-tool="mdnice编辑器">所以到最后，harness 这件事谈的根本不只是 AI。</p>
<p data-tool="mdnice编辑器">谈的是工程纪律怎么重新编码。</p>
<p data-tool="mdnice编辑器">谈的是组织协作怎么重新布线。</p>
<p data-tool="mdnice编辑器">谈的是我们怎么把概率生成系统，放进一个仍然要求长期维护、长期演进、长期负责的软件世界。</p>
<p data-tool="mdnice编辑器">这是我理解的「从 Agent 到组织」。</p>
<p data-tool="mdnice编辑器">以上。</p>
</section>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/05/harness-engineering-anent-org/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>对最近 AI 落地工程实践的一些想法和思考</title>
		<link>https://www.phppan.com/2026/04/some-thoughts-and-reflections-on-recent-ai-implemented-engineering-practices/</link>
		<comments>https://www.phppan.com/2026/04/some-thoughts-and-reflections-on-recent-ai-implemented-engineering-practices/#comments</comments>
		<pubDate>Sat, 25 Apr 2026 00:56:17 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[AI幻觉]]></category>
		<category><![CDATA[AI架构]]></category>
		<category><![CDATA[RAG]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2493</guid>
		<description><![CDATA[最近和小区某上市公司的 CFO 喝茶聊 AI，在过程中思维和实际场景的碰撞，记录如下： 穿透复杂的表象，当前  [&#8230;]]]></description>
				<content:encoded><![CDATA[<p style="color: #424b5d;" data-tool="mdnice编辑器">最近和小区某上市公司的 CFO 喝茶聊 AI，在过程中思维和实际场景的碰撞，记录如下：</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">穿透复杂的表象，当前 LLM 的底层运行逻辑其实非常单一：它本质上是一个自回归的序列生成器，根据已有的上下文，计算词表中每一个 token 出现的概率分布，然后从中采样出下一个 token。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">但这里的「概率」绝非毫无逻辑的随机掷骰子。 这种概率分布，是模型在海量预训练数据中内化的语言规律、世界知识以及逻辑推理能力的数学投影。通过多层 Transformer 网络与注意力机制（Attention），模型在极高的维度上完成了对上下文语义的深度解析与特征关联，从而将符合人类逻辑、契合当前语境的 token 赋予极高的概率权重。它是在用统计学的方式，重现人类的逻辑推理过程。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">然而，无论其内部的概率计算多么精密，从软件工程的宏观视角来看，我们本质上依然是在传统的确定性系统中，强行引入了一个基于概率采样的非确定性组件。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">传统软件工程建立在严格的确定性之上。输入特定的参数，经过固定的业务逻辑，必然得到预期的输出。现在我们将核心逻辑交由概率模型处理，相同的输入在不同的时间点，可能会产生完全不同的输出路径。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">幻觉无法被根除。它是自回归模型的内生特性，是概率采样的必然产物。我们在进行系统架构设计时，必须将幻觉视为系统的常态。试图通过修改 Prompt 来彻底消除幻觉，在工程上徒劳无功。我们需要在系统边界处建立起拦截机制，用确定性的规则去兜底概率模型的不确定性。</p>
<h1 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #e7642b;">容错度决定落地</span></h1>
<p style="color: #424b5d;" data-tool="mdnice编辑器">当前商业化落地最顺畅、ROI 最高的场景，全部集中在高容错度领域。写行业报告、生成营销文案、文生图、视频生成、游戏 NPC 对话。这类场景的核心特征在于缺乏绝对的客观标准。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">在内容创作领域，模型偶尔的逻辑发散会被用户视为创造力。工程团队不需要在接口的绝对可用性和输出的绝对准确性上死磕，只需要保证底线的内容安全和合理的响应延迟。系统可用性达到 95% 就能让用户产生极强的获得感。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">一旦进入低容错度场景，工程实现的复杂度会呈指数级上升。医疗诊断、工业控制、核心交易链路。在这些领域，0.1% 的幻觉率都会导致灾难性的业务后果。我们在评估一个 AI 项目是否立项时，首要考量指标就是业务场景的容错底线。容错度越低，外围需要的确定性校验代码就越厚重，最终会导致系统的维护成本远超 AI 带来的效率提升。</p>
<h1 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #e7642b;">知识外挂 RAG</span></h1>
<p style="color: #424b5d;" data-tool="mdnice编辑器">RAG 的出现是为了解决模型内部知识更新滞后和私有数据隔离的问题。其核心原理是将外部文档切片、向量化，在用户提问时检索相关切片，拼接到 Prompt 中作为上下文喂给大模型。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">在实际的工程环境里，RAG 的核心瓶颈在检索链路。切片策略直接决定了召回质量。按固定 token 长度切分会破坏语义完整性，导致关键信息被腰斩。按标点符号或段落切分会导致切片长度方差过大，影响向量化模型的表达能力。我们在生产环境中通常需要针对不同格式的文档编写定制化的解析器，将 PDF 或 Word 还原为结构化的文档树，再基于文档树的层级进行语义切片。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">单一的向量检索在面对专有名词和长尾词汇时表现极差。我们必须采用混合检索架构：稠密向量检索加上稀疏词表检索。向量检索负责语义泛化，处理同义词和模糊表达。词表检索负责精准匹配产品型号、人名和内部项目代号。混合检索引入了多路召回合并的问题，通常需要引入倒数秩融合算法来重排结果。系统复杂度和查询延迟会成倍增加。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">数据清洗占据了 RAG 项目 80% 的研发精力。直接将企业内部的原始文档灌入向量数据库，最终的问答准确率通常不到 40%。文档中存在大量的废话、过期的流程规范以及相互冲突的条款。垃圾进，垃圾出。我们在构建知识库之前，必须通过脚本和人工介入，对语料进行严格的去重、降噪和结构化提取。</p>
<h1 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #e7642b;">工具调用确定性</span></h1>
<p style="color: #424b5d;" data-tool="mdnice编辑器">为了弥补概率模型的缺陷，我们需要引入确定性的工具。Function Calling 机制本质上是给 LLM 接上双手。模型负责理解自然语言意图并提取结构化参数，具体的业务逻辑交由传统的确定性脚本执行。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器"><strong>工具调用的工程难点在于参数提取的稳定性</strong>。当注册的工具数量超过十个，或者参数结构嵌套层级过深时，模型的输出格式极易崩溃。我们在中间层必须加入严格的 Schema 校验机制。一旦校验失败，需要截断错误信息并触发重试。重试次数上限通常设定为 3 次，继续增加会耗尽上下文窗口并导致请求超时。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">多轮工具调用会带来严重的延迟问题。模型每决定调用一次工具，都需要经历一次完整的网络请求和推理过程。如果一个复杂任务需要串行调用三个工具，用户的等待时间会轻易突破 10 秒。我们在架构设计时，需要尽可能将细粒度的 API 聚合成粗粒度的宏接口，<strong>减少模型与业务系统的交互频次</strong>。</p>
<h1 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #e7642b;">Agent 架构的脆弱性与状态管理</span></h1>
<p style="color: #424b5d;" data-tool="mdnice编辑器">多智能体（Multi-Agent）架构在技术社区被过度神话。多个大模型相互协作、自主规划任务的 Demo 看起来非常惊艳。在真实的工业场景中，完全由 LLM 自主驱动的 Agent 链路极其脆弱。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">误差会在多步推理中被迅速放大。假设单个 Agent 节点的输出准确率为 90%，一个包含五个节点的串行任务，最终的成功率会暴跌至 59%。任何一个节点的幻觉都会导致后续链路彻底跑偏。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">我们在生产环境中构建复杂任务流时，坚决摒弃由 LLM 自主决定执行路径的黑盒模式。控制流必须由传统的有向无环图（DAG）或状态机来接管。LLM 仅仅作为状态机中的一个计算节点，负责处理非结构化数据的理解和生成。节点与节点之间的状态流转、条件判断、异常重试，全部由确定性的代码实现。这种设计牺牲了系统的灵活性，换取了业务系统必须具备的稳定性和可观测性。</p>
<h1 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #e7642b;">非确定性系统的测试与监控</span></h1>
<p style="color: #424b5d;" data-tool="mdnice编辑器">非确定性系统的测试与监控，是传统软件工程团队转型 AI 开发时遇到的最大痛点。传统的单元测试基于断言，期望输出是固定的字符串或数值。面对 LLM 每次都不一样的回答，基于精确匹配的 CI/CD 流水线会全线崩溃。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">我们重构了整个测试评估体系。引入 LLM-as-a-Judge 机制，使用一个能力更强、参数规模更大的模型来评估业务模型的输出质量。评估维度被拆解为相关性、事实一致性、格式合规性等具体指标。在每次模型版本迭代或 Prompt 修改后，必须在包含上千个真实业务 Case 的黄金数据集上运行自动化评估。只有各项指标的波动在可控范围内，才能进行灰度发布。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">在监控层面，传统的 APM 工具无法满足需求。我们需要采集每一个请求的 Prompt 模板版本、输入变量、输出结果、Token 消耗量以及推理延迟。这些数据是后续进行 Bad Case 分析和模型微调的唯一原料。针对 Token 消耗的监控直接与业务成本挂钩。我们会在网关层设置严格的并发限制和预算熔断机制，防止恶意请求或死循环调用导致账单失控。</p>
<h1 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #e7642b;">两种范式的碰撞</span></h1>
<p style="color: #424b5d;" data-tool="mdnice编辑器">AI First 与 AI 辅助是完全不同的架构逻辑。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">AI 辅助是在现有系统中打补丁。主干流程依然是传统的表单和按钮，AI 作为一个侧边栏或悬浮窗存在，提供总结、翻译、润色功能。开发成本极低，对原有系统无侵入。用户在遇到问题时，可以选择性地向 AI 求助。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">AI First 要求重构整个交互形态和底层流转逻辑。系统不再依赖预设的菜单树，由 LLM 充当中央路由。用户的自然语言输入直接驱动底层状态机流转。这要求所有内部 API 具备极高的自描述能力，业务逻辑必须高度解耦。我们在推进 AI First 架构时，面临的最大阻力通常来自老旧系统的技术债。历史遗留的紧耦合代码根本无法被封装成独立的工具供模型调用。</p>
<h1 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #e7642b;">财务场景的拆解</span></h1>
<p style="color: #424b5d;" data-tool="mdnice编辑器">财务场景是典型的低容错度、高确定性要求的领域。将概率模型直接应用于财务核心链路会引发严重的合规风险。可落地的切入点集中在外围的非结构化数据处理和信息流转环节。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">发票与报销单据的信息抽取是一个高价值场景。传统 OCR 结合正则匹配在面对版式多变的票据时维护成本极高。引入大模型进行多模态信息抽取，将非结构化的图片或 PDF 转换为结构化的 JSON 数据。抽取后的数据必须经过传统规则引擎的二次校验，例如金额试算平衡验证、税号合规性检查。模型在这里承担的是「粗加工」角色，最终的业务落库动作依然由确定性代码把控。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">财务制度问答可以大幅降低沟通成本。基于企业内部报销规范构建 RAG 系统。员工在提单前通过自然语言查询报销标准。这里的 RAG 必须严格限制模型的发散，Prompt 中需强制要求「仅根据检索到的内容回答，未提及的内容直接回复不知道」。为了防止模型编造财务政策，我们会在输出层增加一层文本相似度校验，确保模型的回答与检索到的原文保持高度一致。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">财务分析报告初稿生成也是一个可行的方向。将结构化的财务报表数据通过代码转换为文本描述，作为上下文喂给模型，让其生成趋势分析和异常波动提示。模型在这里仅作为「翻译官」和「排版员」，不参与任何数值计算。所有的同比、环比计算必须在传统代码层完成，将计算结果以明确的数值形式提供给模型。让 LLM 去做算术题是工程上的反模式。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">数据隐私在财务场景中是不可逾越的红线。公有云 API 无法满足审计要求。我们通常需要采用本地私有化部署的开源模型。7B 到 14B 参数规模的模型经过量化处理后，可以在单张消费级显卡上流畅运行。通过针对财务语料的微调，这些小模型在特定信息抽取任务上的表现可以持平甚至超越千亿参数的通用大模型。私有化部署带来了硬件采购和模型运维的额外成本，需要在项目初期进行严格的 ROI 测算。</p>
<p style="color: #424b5d;" data-tool="mdnice编辑器">以上</p>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/04/some-thoughts-and-reflections-on-recent-ai-implemented-engineering-practices/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Claude Code 的 SKILLS 技能渐进式披露实现原理解析</title>
		<link>https://www.phppan.com/2026/04/claude-code-ai-skills-source/</link>
		<comments>https://www.phppan.com/2026/04/claude-code-ai-skills-source/#comments</comments>
		<pubDate>Sun, 12 Apr 2026 03:47:23 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[ClaudeCode]]></category>
		<category><![CDATA[skills]]></category>
		<category><![CDATA[渐进式披露]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2490</guid>
		<description><![CDATA[SKILLS 和 渐进式披露 是 A 家最早提出来的方案，也是 OpenClaw 火了后大家一直讨论的哪个技能 [&#8230;]]]></description>
				<content:encoded><![CDATA[<section style="color: #000000;" data-tool="mdnice编辑器" data-website="https://www.mdnice.com" data-pm-slice="0 0 []">
<p data-tool="mdnice编辑器">SKILLS 和 <strong style="color: #0e88eb;">渐进式披露</strong> 是 A 家最早提出来的方案，也是 OpenClaw 火了后大家一直讨论的哪个技能好用很核心的强依赖的实现逻辑。</p>
<p data-tool="mdnice编辑器">如果把 Claude Code 的 skills 理解成一堆 prompt 文件，后面的很多设计都解释不通。</p>
<p data-tool="mdnice编辑器">从其源码实现来看，会发现它在解决的核心问题是：<strong style="color: #0e88eb;">怎么让模型保留足够强的技能召回能力，同时又不把常驻上下文撑爆。</strong></p>
<p data-tool="mdnice编辑器">这件事说穿了就是五个字：<strong style="color: #0e88eb;">渐进式披露</strong>。</p>
<p data-tool="mdnice编辑器">大概的逻辑是：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">先告诉模型「系统里存在 skills 机制」。</section>
</li>
<li>
<section style="color: #010101;">再告诉它「当前有哪些 skill 名称和简短说明」。</section>
</li>
<li>
<section style="color: #010101;">等它真的决定调用某个 skill 时，再把正文、权限、hooks、模型覆盖、附加工具权限这些重内容展开。</section>
</li>
<li>
<section style="color: #010101;">如果某些 skill 还和路径、目录、文件类型绑定，那就继续往后拖，拖到模型真的碰到对应文件时再激活。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这是一个优雅且干净的工程化设计。它没有发明一套复杂到难以维护的 skill runtime，也没有把所谓智能寄托在黑盒检索器上，而是先把「披露成本」这件事控制住。</p>
<p data-tool="mdnice编辑器">我们按工程实现往下拆：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">skill 在系统里到底被建模成什么</section>
</li>
<li>
<section style="color: #010101;">多来源 skill 是怎么统一装配的</section>
</li>
<li>
<section style="color: #010101;">渐进式披露具体分了哪几层</section>
</li>
<li>
<section style="color: #010101;">条件激活和动态发现是怎么接进文件操作链路的</section>
</li>
<li>
<section style="color: #010101;">inline 和 fork 两条执行路径分别解决什么问题</section>
</li>
<li>
<section style="color: #010101;">这套设计真正适合什么场景，代价又是什么</section>
</li>
<li>
<section style="color: #010101;">如果要在自己的 Agent 里复刻，最短落地路径应该怎么走</section>
</li>
</ul>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">一、先看 skills 在系统里被建模成什么</span></h1>
<p data-tool="mdnice编辑器">Claude Code 里，skill 最终会被统一建模成 <code style="color: #0e8aeb;">Command</code>，而且类型是 <code style="color: #0e8aeb;">prompt</code>。</p>
<p data-tool="mdnice编辑器">最核心的构造函数是 createSkillCommand：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">return</span> {
<span style="color: #c678dd;">type</span>: <span style="color: #98c379;">'prompt'</span>,
  name: skillName,
  description,
  hasUserSpecifiedDescription,
  allowedTools,
  argumentHint,
  argNames: argumentNames.length &gt; <span style="color: #d19a66;">0</span> ? argumentNames : <span style="color: #56b6c2;">undefined</span>,
  whenToUse,
  version,
  model,
  disableModelInvocation,
  userInvocable,
  context: executionContext,
  agent,
  effort,
  paths,
  contentLength: markdownContent.length,
  isHidden: !userInvocable,
  progressMessage: <span style="color: #98c379;">'running'</span>,
  userFacingName(): <span style="color: #e6c07b;">string</span> {
    <span style="color: #c678dd;">return</span> displayName || skillName
  },
  source,
  loadedFrom,
  hooks,
  skillRoot: baseDir,
<span style="color: #c678dd;">async</span> getPromptForCommand(args, toolUseContext) {
    ...
    <span style="color: #c678dd;">return</span> [{ <span style="color: #c678dd;">type</span>: <span style="color: #98c379;">'text'</span>, text: finalContent }]
  },
}
</code></pre>
<p data-tool="mdnice编辑器">这段代码说明有几个关键点：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">skill 不是特殊 runtime object，而是 <code style="color: #0e8aeb;">prompt command</code></section>
</li>
<li>
<section style="color: #010101;">skill 本体是 <code style="color: #0e8aeb;">getPromptForCommand()</code> 生成的一组文本 block</section>
</li>
<li>
<section style="color: #010101;">skill 可以带：</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">allowedTools</code></section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">model</code></section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">effort</code></section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">paths</code></section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">hooks</code></section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">context: inline | fork</code></section>
</li>
</ul>
</li>
<li>
<section style="color: #010101;">skill 的调用结果，不是「执行一段脚本」，而是<strong style="color: #0e88eb;">把 skill 展开成后续对话消息，或者 fork 成子代理执行</strong></section>
</li>
</ul>
<p data-tool="mdnice编辑器">如果我们自己做 Agent，建议参考。skill 不要单独发明一套 DSL runtime，直接把它抽象成「可延迟展开的 prompt 命令」就够了。</p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">二、skills 的来源有哪几类</span></h1>
<p data-tool="mdnice编辑器">skills 并不只来自一个目录。<code style="color: #0e8aeb;">getSkills()</code> 会把多个来源统一聚合。[commands.ts] commands.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L353</a>-L398</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">const</span> [skillDirCommands, pluginSkills] = <span style="color: #c678dd;">await</span> <span style="color: #e6c07b;">Promise</span>.all([
  getSkillDirCommands(cwd)...
  getPluginSkills()...
])
<span style="color: #c678dd;">const</span> bundledSkills = getBundledSkills()
<span style="color: #c678dd;">const</span> builtinPluginSkills = getBuiltinPluginSkillCommands()
</code></pre>
<p data-tool="mdnice编辑器">然后 <code style="color: #0e8aeb;">loadAllCommands()</code> 再把这些东西和 workflow/plugin/内建命令一起合并。[commands.ts] commands.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L445</a>-L469</p>
<p data-tool="mdnice编辑器">也就是说，skills 的来源至少有：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">bundled skills</section>
</li>
<li>
<section style="color: #010101;">磁盘上的 <code style="color: #0e8aeb;">/skills/</code></section>
</li>
<li>
<section style="color: #010101;">plugin skills</section>
</li>
<li>
<section style="color: #010101;">builtin plugin skills</section>
</li>
<li>
<section style="color: #010101;">兼容旧 <code style="color: #0e8aeb;">/commands/</code> 目录加载进来的 prompt commands</section>
</li>
</ul>
<p data-tool="mdnice编辑器"><strong style="color: #0e88eb;">SkillTool 根本不需要知道 skill 来自哪里</strong>。只要最后是 <code style="color: #0e8aeb;">prompt command</code>，就能走统一调用路径。</p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">三、skills 的「渐进式披露」分 5 层</span></h1>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">1）第一层：系统提示只声明「技能机制存在」</span></h2>
<p data-tool="mdnice编辑器">系统提示里不会把所有 skill 正文直接塞进去。它只给一个能力声明，告诉模型：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">用户说 <code style="color: #0e8aeb;">/&lt;skill-name&gt;</code>，其实是在指 skill</section>
</li>
<li>
<section style="color: #010101;">可以用 <code style="color: #0e8aeb;">SkillTool</code> 去执行</section>
</li>
<li>
<section style="color: #010101;">不要乱猜，只能调用列出来的那些</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这段在 [prompts.ts] prompts.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L353</a>-L401：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;">hasSkills
  ? <span style="color: #98c379;">`/&lt;skill-name&gt; (e.g., /commit) is shorthand for users to invoke a user-invocable skill. When executed, the skill gets expanded to a full prompt. Use the <span style="color: #e06c75;">${SKILL_TOOL_NAME}</span> tool to execute them. IMPORTANT: Only use <span style="color: #e06c75;">${SKILL_TOOL_NAME}</span> for skills listed in its user-invocable skills section - do not guess or use built-in CLI commands.`</span>
  : <span style="color: #56b6c2;">null</span>
</code></pre>
<p data-tool="mdnice编辑器">这一步只暴露了<strong style="color: #0e88eb;">机制</strong>，没有暴露<strong style="color: #0e88eb;">内容</strong>。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">2）第二层：只披露 skill 名称和短描述</span></h2>
<p data-tool="mdnice编辑器">真正给模型看的 skill 列表，是通过 <code style="color: #0e8aeb;">getSkillToolCommands()</code> 过滤出来的。[commands.ts] commands.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L561</a>-L580</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">return</span> allCommands.filter(
  cmd =&gt;
    cmd.type === <span style="color: #98c379;">'prompt'</span> &amp;&amp;
    !cmd.disableModelInvocation &amp;&amp;
    cmd.source !== <span style="color: #98c379;">'builtin'</span> &amp;&amp;
    (
      cmd.loadedFrom === <span style="color: #98c379;">'bundled'</span> ||
      cmd.loadedFrom === <span style="color: #98c379;">'skills'</span> ||
      cmd.loadedFrom === <span style="color: #98c379;">'commands_DEPRECATED'</span> ||
      cmd.hasUserSpecifiedDescription ||
      cmd.whenToUse
    ),
)
</code></pre>
<p data-tool="mdnice编辑器">这段有两个要点：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">只有 <code style="color: #0e8aeb;">prompt</code> 命令才能进 skill 列表</section>
</li>
<li>
<section style="color: #010101;">并不是所有 prompt command 都自动暴露，至少得满足可描述性要求</section>
</li>
</ul>
<p data-tool="mdnice编辑器">也就是说，<strong style="color: #0e88eb;">可执行集合</strong>和<strong style="color: #0e88eb;">对模型披露集合</strong>不是完全相同的。<br />
Claude Code 在这里收了一刀，避免模型看到一堆没有描述、无法判断用途的技能。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">3）第三层：列表本身还要走预算裁剪</span></h2>
<p data-tool="mdnice编辑器">skill 列表不是全量原文塞进 prompt，而是按预算压缩过的。核心逻辑在 [prompt.ts] tools/SkillTool/prompt.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L20</a>-L171。</p>
<p data-tool="mdnice编辑器">最关键的常量：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">export</span> <span style="color: #c678dd;">const</span> SKILL_BUDGET_CONTEXT_PERCENT = <span style="color: #d19a66;">0.01</span>
<span style="color: #c678dd;">export</span> <span style="color: #c678dd;">const</span> DEFAULT_CHAR_BUDGET = <span style="color: #d19a66;">8</span>_000
<span style="color: #c678dd;">export</span> <span style="color: #c678dd;">const</span> MAX_LISTING_DESC_CHARS = <span style="color: #d19a66;">250</span>
</code></pre>
<p data-tool="mdnice编辑器">以及格式化逻辑：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">return</span> <span style="color: #98c379;">`- <span style="color: #e06c75;">${cmd.name}</span>: <span style="color: #e06c75;">${getCommandDescription(cmd)}</span>`</span>
</code></pre>
<p data-tool="mdnice编辑器">和预算裁剪：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">if</span> (fullTotal &lt;= budget) {
  <span style="color: #c678dd;">return</span> fullEntries.map(e =&gt; e.full).join(<span style="color: #98c379;">'\n'</span>)
}
</code></pre>
<p data-tool="mdnice编辑器">如果超预算，就会：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">bundled skills 尽量保留完整描述</section>
</li>
<li>
<section style="color: #010101;">其它 skills 截断 description</section>
</li>
<li>
<section style="color: #010101;">极端情况下退化成只发 <code style="color: #0e8aeb;">- skill-name</code></section>
</li>
</ul>
<p data-tool="mdnice编辑器">这就是很典型的渐进式披露：<strong style="color: #0e88eb;">先给最小可用索引，不给正文</strong>。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">4）第四层：列表还是增量下发，不是每轮全量重发</span></h2>
<p data-tool="mdnice编辑器">技能列表通过 <code style="color: #0e8aeb;">skill_listing</code> attachment 发给模型。发送逻辑在 [attachments.ts] utils/attachments.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L2669</a>-L2752。</p>
<p data-tool="mdnice编辑器">核心逻辑：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">const</span> newSkills = allCommands.filter(cmd =&gt; !sent.has(cmd.name))
...
<span style="color: #c678dd;">for</span> (<span style="color: #c678dd;">const</span> cmd of newSkills) {
  sent.add(cmd.name)
}
...
<span style="color: #c678dd;">return</span> [
  {
    <span style="color: #c678dd;">type</span>: <span style="color: #98c379;">'skill_listing'</span>,
    content,
    skillCount: newSkills.length,
    isInitial,
  },
]
</code></pre>
<p data-tool="mdnice编辑器">这个 <code style="color: #0e8aeb;">sentSkillNames</code> 机制说明：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">第一次发的是初始批次</section>
</li>
<li>
<section style="color: #010101;">后面只发新增的 skill</section>
</li>
<li>
<section style="color: #010101;">resume 之后还会 suppress，避免重复污染上下文</section>
</li>
</ul>
<p data-tool="mdnice编辑器">然后 <code style="color: #0e8aeb;">messages.ts</code> 会把它包成系统提醒。[messages.ts] utils/messages.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L3763</a>-L3772</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">return</span> wrapMessagesInSystemReminder([
  createUserMessage({
    content: <span style="color: #98c379;">`The following skills are available for use with the Skill tool:\n\n<span style="color: #e06c75;">${attachment.content}</span>`</span>,
    isMeta: <span style="color: #56b6c2;">true</span>,
  }),
])
</code></pre>
<p data-tool="mdnice编辑器">很多 Agent 会每轮把所有 tools / skills 全量重发，Claude Code 显然在认真控 token。 当然，如果技能不多，也可以直接全量发，不要过早优化。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">5）第五层：真正的 skill 内容延迟到调用时才展开</span></h2>
<p data-tool="mdnice编辑器">直到调用 <code style="color: #0e8aeb;">SkillTool</code>，skill 的真实正文才会通过 <code style="color: #0e8aeb;">command.getPromptForCommand()</code> 生成。[SkillTool.ts] utils/processUserInput/processSlashCommand.tsx<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L869</a>-L920</p>
<p data-tool="mdnice编辑器">这里才会发生：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">$ARGUMENTS</code> 替换</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">${CLAUDE_SKILL_DIR}</code> 替换</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">${CLAUDE_SESSION_ID}</code> 替换</section>
</li>
<li>
<section style="color: #010101;">markdown 内嵌 shell 执行</section>
</li>
<li>
<section style="color: #010101;">hooks 注册</section>
</li>
<li>
<section style="color: #010101;">附加权限 attachment 注入</section>
</li>
<li>
<section style="color: #010101;">invoked skill 记录</section>
</li>
</ul>
<p data-tool="mdnice编辑器">换句话说，skill 的重内容、重权限、重上下文副作用，都是<strong style="color: #0e88eb;">按需加载</strong>。</p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">四、除了延迟加载，它还做了「条件激活」</span></h1>
<p data-tool="mdnice编辑器">这也是渐进式披露的重要一层，而且很多人会漏掉。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">1）带 <code>paths</code> frontmatter 的 skill，不会启动即暴露</span></h2>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">getSkillDirCommands()</code> 里会把 skill 分成两类：[loadSkillsDir.ts] loadSkillsDir.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L771</a>-L803</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">if</span> (
  skill.type === <span style="color: #98c379;">'prompt'</span> &amp;&amp;
  skill.paths &amp;&amp;
  skill.paths.length &gt; <span style="color: #d19a66;">0</span> &amp;&amp;
  !activatedConditionalSkillNames.has(skill.name)
) {
  newConditionalSkills.push(skill)
} <span style="color: #c678dd;">else</span> {
  unconditionalSkills.push(skill)
}
</code></pre>
<p data-tool="mdnice编辑器">然后 conditional skills 被先放进 <code style="color: #0e8aeb;">conditionalSkills</code> map，而不是直接进入模型可见集合。</p>
<p data-tool="mdnice编辑器">这意味着：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">你定义了某个 skill 只适用于 <code style="color: #0e8aeb;">*.tsx</code></section>
</li>
<li>
<section style="color: #010101;">它不会在项目启动时就干扰所有任务</section>
</li>
<li>
<section style="color: #010101;">只有模型真的碰到匹配文件时，这个 skill 才会被激活</section>
</li>
</ul>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">2）激活时机挂在文件操作上</span></h2>
<p data-tool="mdnice编辑器">FileRead / FileWrite / FileEdit 三个工具里，都有两步副作用：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">发现上层目录里的 <code style="color: #0e8aeb;">.claude/skills</code></section>
</li>
<li>
<section style="color: #010101;">激活匹配当前文件路径的 conditional skills</section>
</li>
</ul>
<p data-tool="mdnice编辑器">比如 FileReadTool：[FileReadTool.ts] /tools/FileReadTool/FileReadTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L575</a>-L591</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">const</span> newSkillDirs = <span style="color: #c678dd;">await</span> discoverSkillDirsForPaths([fullFilePath], cwd)
...
addSkillDirectories(newSkillDirs).catch(() =&gt; {})
...
activateConditionalSkillsForPaths([fullFilePath], cwd)
</code></pre>
<p data-tool="mdnice编辑器">对应的激活实现是 [activateConditionalSkillsForPaths] skills/loadSkillsDir.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L997</a>-L1058：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">const</span> skillIgnore = ignore().add(skill.paths)
...
<span style="color: #c678dd;">if</span> (skillIgnore.ignores(relativePath)) {
  dynamicSkills.set(name, skill)
  conditionalSkills.delete(name)
  activatedConditionalSkillNames.add(name)
}
</code></pre>
<p data-tool="mdnice编辑器">这一步非常像条件规则系统，而不是纯静态注册。<br />
效果就是：<strong style="color: #0e88eb;">技能集合会随着你读写哪些文件而变化</strong>。</p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">五、动态发现本身也是渐进式披露的一部分</span></h1>
<p data-tool="mdnice编辑器">除了 path-conditional activation，Claude Code 还支持<strong style="color: #0e88eb;">目录级动态发现</strong>。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">1）启动时只加载一部分 skill 目录</span></h2>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">getSkillDirCommands()</code> 启动时会加载：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">managed</section>
</li>
<li>
<section style="color: #010101;">user</section>
</li>
<li>
<section style="color: #010101;">project dirs</section>
</li>
<li>
<section style="color: #010101;">additional dirs</section>
</li>
<li>
<section style="color: #010101;">legacy commands</section>
</li>
</ul>
<p data-tool="mdnice编辑器">但它不会把所有嵌套目录里的 <code style="color: #0e8aeb;">.claude/skills</code> 一次性全扫出来。[loadSkillsDir.ts] skills/loadSkillsDir.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L638</a>-L804</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">2）当模型碰到某个文件时，再向上走目录树找嵌套 skill</span></h2>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">discoverSkillDirsForPaths()</code> 会从当前文件的父目录开始，一路往上走到 cwd，查找 <code style="color: #0e8aeb;">.claude/skills</code>。[loadSkillsDir.ts] skills/loadSkillsDir.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L861</a>-L915</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">while</span> (currentDir.startsWith(resolvedCwd + pathSep)) {
  <span style="color: #c678dd;">const</span> skillDir = join(currentDir, <span style="color: #98c379;">'.claude'</span>, <span style="color: #98c379;">'skills'</span>)
  ...
  <span style="color: #c678dd;">await</span> fs.stat(skillDir)
  ...
  newDirs.push(skillDir)
}
</code></pre>
<p data-tool="mdnice编辑器">而且还做了两个非常实用的约束：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">已检查过的目录不会重复 stat</section>
</li>
<li>
<section style="color: #010101;">gitignored 目录里的 skills 不会静默加载</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这个设计让：<br />
<strong style="color: #0e88eb;">技能跟着你进入子目录而出现，不跟整个仓库一起一次性曝光。</strong></p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">六、SkillTool 的调用链，实际上分 inline 和 fork 两条路</span></h1>
<p data-tool="mdnice编辑器">这是技能系统和普通 slash command 最大的不同之一。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">1）调用前校验</span></h2>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">SkillTool.validateInput()</code> 会做：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">去掉前导 <code style="color: #0e8aeb;">/</code></section>
</li>
<li>
<section style="color: #010101;">检查 skill 是否存在</section>
</li>
<li>
<section style="color: #010101;">检查是否 <code style="color: #0e8aeb;">disableModelInvocation</code></section>
</li>
<li>
<section style="color: #010101;">检查是否为 <code style="color: #0e8aeb;">prompt</code> 类型<br />
见 [SkillTool.ts] tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L355</a>-L430</section>
</li>
</ul>
<p data-tool="mdnice编辑器">关键逻辑：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">const</span> commands = <span style="color: #c678dd;">await</span> getAllCommands(context)
<span style="color: #c678dd;">const</span> foundCommand = findCommand(normalizedCommandName, commands)
...
<span style="color: #c678dd;">if</span> (foundCommand.type !== <span style="color: #98c379;">'prompt'</span>) {
  <span style="color: #c678dd;">return</span> {
    result: <span style="color: #56b6c2;">false</span>,
    message: <span style="color: #98c379;">`Skill <span style="color: #e06c75;">${normalizedCommandName}</span> is not a prompt-based skill`</span>,
  }
}
</code></pre>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">2）权限检查</span></h2>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">SkillTool.checkPermissions()</code> 很细，除了 allow / deny 规则，还会对「只有安全属性的 skill」自动放行。[SkillTool.ts] /tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L433</a>-L579</p>
<p data-tool="mdnice编辑器">这个设计的意义是：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">简单 declarative skill 不必每次都弹权限</section>
</li>
<li>
<section style="color: #010101;">带额外风险属性的 skill 要 ask user</section>
</li>
</ul>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">3）inline skill：展开成后续对话消息</span></h2>
<p data-tool="mdnice编辑器">默认分支会走 <code style="color: #0e8aeb;">processPromptSlashCommand()</code>。[SkillTool.ts] tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L635</a>-L644</p>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">getMessagesForPromptSlashCommand()</code> 干的事情很丰富：[processSlashCommand.tsx] utils/processUserInput/processSlashCommand.tsx<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L827</a>-L920</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">command.getPromptForCommand(args, context)</code> 得到真正 skill 正文</section>
</li>
<li>
<section style="color: #010101;">注册 hooks</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">addInvokedSkill()</code> 记录 skill 内容，供 compact 时恢复</section>
</li>
<li>
<section style="color: #010101;">从 skill 文本里再抽 attachment</section>
</li>
<li>
<section style="color: #010101;">增加 <code style="color: #0e8aeb;">command_permissions</code> attachment</section>
</li>
<li>
<section style="color: #010101;">生成一批 <code style="color: #0e8aeb;">messages</code></section>
</li>
</ul>
<p data-tool="mdnice编辑器">返回结构里最关键的是：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">return</span> {
  messages,
  shouldQuery: <span style="color: #56b6c2;">true</span>,
  allowedTools: additionalAllowedTools,
  model: command.model,
  effort: command.effort,
  command
}
</code></pre>
<p data-tool="mdnice编辑器">也就是说，inline skill 的本质是：<br />
<strong style="color: #0e88eb;">把 skill 变成一段新的上下文和权限修饰，然后让主对话继续跑。</strong></p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">4）fork skill：交给子代理跑，再把结果归还</span></h2>
<p data-tool="mdnice编辑器">如果 skill frontmatter 里声明 <code style="color: #0e8aeb;">context === 'fork'</code>，就走 <code style="color: #0e8aeb;">executeForkedSkill()</code>。[SkillTool.ts] tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L622</a>-L633</p>
<p data-tool="mdnice编辑器">它会：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">构造子代理上下文</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">runAgent()</code></section>
</li>
<li>
<section style="color: #010101;">收集 agent messages</section>
</li>
<li>
<section style="color: #010101;">抽取结果文本</section>
</li>
<li>
<section style="color: #010101;">最终返回 <code style="color: #0e8aeb;">{ status: 'forked', agentId, result }</code><br />
见 [executeForkedSkill] /tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L122</a>-L290</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这一步说明 Claude Code 已经把 skill 分成两类：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">知识/流程模板型 skill</strong>：inline 展开</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">工作委派型 skill</strong>：fork 子代理执行</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这个值得学一下。不是所有 skill 都应该展开在主上下文里。</p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">七、结果返回逻辑</span></h1>
<p data-tool="mdnice编辑器">为什么它也算渐进式披露的一部分？</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">1）inline skill 的 tool_result</span></h2>
<p data-tool="mdnice编辑器">很轻</p>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">mapToolResultToToolResultBlockParam()</code> 对 inline skill 的返回只是：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;">content: <span style="color: #98c379;">`Launching skill: <span style="color: #e06c75;">${result.commandName}</span>`</span>
</code></pre>
<p data-tool="mdnice编辑器">见 [SkillTool.ts] tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L857</a>-L862</p>
<p data-tool="mdnice编辑器">也就是说，tool_result 本身不承载 skill 的全部结果。<br />
真正有价值的内容在 <code style="color: #0e8aeb;">newMessages</code> 里，已经被送回主会话继续推理。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">2）fork skill 的 tool_result</span></h2>
<p data-tool="mdnice编辑器">直接带最终结果</p>
<p data-tool="mdnice编辑器">fork skill 返回的是：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;">content: <span style="color: #98c379;">`Skill "<span style="color: #e06c75;">${result.commandName}</span>" completed (forked execution).\n\nResult:\n<span style="color: #e06c75;">${result.result}</span>`</span>
</code></pre>
<p data-tool="mdnice编辑器">见 [SkillTool.ts] tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L848</a>-L855</p>
<p data-tool="mdnice编辑器">这是因为 fork skill 已经在独立上下文里把工作做完了，主线程要拿的是总结结果。</p>
<p data-tool="mdnice编辑器">所以在 Claude Code 里，skill 结果返回不是单一模式，而是：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">inline：返回「已加载 skill」，真正内容进主对话</section>
</li>
<li>
<section style="color: #010101;">fork：返回「子代理执行结果」</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这也是一种披露控制。<br />
不同执行语义，对结果暴露方式也不同。</p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">八、如何简要实现</span></h1>
<p data-tool="mdnice编辑器">一个新 Agent，如何简要实现 skills 的发现、召回、调用、结果返回？</p>
<p data-tool="mdnice编辑器">一个<strong style="color: #0e88eb;">够用、够短、能落地</strong>的最小设计，不追求和 Claude Code 一模一样，但核心思路一致。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">1）第一步：统一 skill 数据结构</span></h2>
<p data-tool="mdnice编辑器">最小结构建议这样：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">type</span> Skill = {
  name: <span style="color: #e6c07b;">string</span>
  description: <span style="color: #e6c07b;">string</span>
  whenToUse?: <span style="color: #e6c07b;">string</span>
  contentLoader: (args: <span style="color: #e6c07b;">string</span>, ctx: AgentContext) =&gt; <span style="color: #e6c07b;">Promise</span>&lt;<span style="color: #e6c07b;">string</span>&gt;
  allowedTools?: <span style="color: #e6c07b;">string</span>[]
  model?: <span style="color: #e6c07b;">string</span>
  effort?: <span style="color: #98c379;">'low'</span> | <span style="color: #98c379;">'medium'</span> | <span style="color: #98c379;">'high'</span>
  context?: <span style="color: #98c379;">'inline'</span> | <span style="color: #98c379;">'fork'</span>
  paths?: <span style="color: #e6c07b;">string</span>[]
}
</code></pre>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">contentLoader</code> 允许延迟展开</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">context</code> 决定 inline/fork</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">paths</code> 支持条件激活</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">allowedTools/model/effort</code> 支持 skill 级上下文修饰</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这和 Claude Code 的 <code style="color: #0e8aeb;">createSkillCommand()</code> 思路是一致的。[loadSkillsDir.ts] skills/loadSkillsDir.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L270</a>-L401</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">2）第二步：启动时只加载「索引」，不要加载正文</span></h2>
<p data-tool="mdnice编辑器">最简做法：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">扫描 skills 目录</section>
</li>
<li>
<section style="color: #010101;">解析 frontmatter</section>
</li>
<li>
<section style="color: #010101;">只把 <code style="color: #0e8aeb;">name / description / whenToUse / paths / context</code> 放进 registry</section>
</li>
<li>
<section style="color: #010101;">skill 正文不要此时进 prompt</section>
</li>
</ul>
<p data-tool="mdnice编辑器">示意：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">async</span> <span style="color: #c678dd;">function</span> <span style="color: #61aeee;">loadSkillIndex</span>(skillDirs: <span style="color: #e6c07b;">string</span>[]): <span style="color: #61aeee;">Promise</span>&lt;<span style="color: #61aeee;">Skill</span>[]&gt; {
<span style="color: #c678dd;">const</span> skills: Skill[] = []
<span style="color: #c678dd;">for</span> (<span style="color: #c678dd;">const</span> dir of skillDirs) {
    <span style="color: #c678dd;">for</span> (<span style="color: #c678dd;">const</span> skillFile of <span style="color: #c678dd;">await</span> listSkillFiles(dir)) {
      <span style="color: #c678dd;">const</span> raw = <span style="color: #c678dd;">await</span> readFile(skillFile, <span style="color: #98c379;">'utf8'</span>)
      <span style="color: #c678dd;">const</span> { frontmatter, content } = parseFrontmatter(raw)
      skills.push({
        name: basename(dirname(skillFile)),
        description: <span style="color: #e6c07b;">String</span>(frontmatter.description ?? <span style="color: #98c379;">''</span>),
        whenToUse: frontmatter.when_to_use ? <span style="color: #e6c07b;">String</span>(frontmatter.when_to_use) : <span style="color: #56b6c2;">undefined</span>,
        paths: <span style="color: #e6c07b;">Array</span>.isArray(frontmatter.paths) ? frontmatter.paths : <span style="color: #56b6c2;">undefined</span>,
        context: frontmatter.context === <span style="color: #98c379;">'fork'</span> ? <span style="color: #98c379;">'fork'</span> : <span style="color: #98c379;">'inline'</span>,
        contentLoader: <span style="color: #c678dd;">async</span> () =&gt; content,
      })
    }
  }
<span style="color: #c678dd;">return</span> skills
}
</code></pre>
<p data-tool="mdnice编辑器">这个阶段要学 Claude Code 的不是目录细节，而是<strong style="color: #0e88eb;">索引和正文分离</strong>。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">3）第三步：做一个「未发送 skill 集合」</span></h2>
<p data-tool="mdnice编辑器">这是渐进式披露的核心。</p>
<p data-tool="mdnice编辑器">维护一个 session 级状态：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">type</span> SkillDisclosureState = {
  sentSkillNames: Set&lt;<span style="color: #e6c07b;">string</span>&gt;
}
</code></pre>
<p data-tool="mdnice编辑器">每轮只发送新的：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">function</span> <span style="color: #61aeee;">getNewSkillListings</span>(skills: Skill[], sent: Set&lt;<span style="color: #e6c07b;">string</span>&gt;): <span style="color: #61aeee;">Skill</span>[] {
  <span style="color: #c678dd;">const</span> fresh = skills.filter(s =&gt; !sent.has(s.name))
  <span style="color: #c678dd;">for</span> (<span style="color: #c678dd;">const</span> s of fresh) sent.add(s.name)
  <span style="color: #c678dd;">return</span> fresh
}
</code></pre>
<p data-tool="mdnice编辑器">然后把它格式化成短列表，而不是全文：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">function</span> <span style="color: #61aeee;">formatSkillListing</span>(skills: Skill[]): <span style="color: #61aeee;">string</span> {
  <span style="color: #c678dd;">return</span> skills.map(s =&gt; <span style="color: #98c379;">`- <span style="color: #e06c75;">${s.name}</span>: <span style="color: #e06c75;">${s.description}</span>`</span>).join(<span style="color: #98c379;">'\n'</span>)
}
</code></pre>
<p data-tool="mdnice编辑器">这对应 Claude Code 的 <code style="color: #0e8aeb;">sentSkillNames + skill_listing attachment</code> 方案。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">4）第四步：把文件操作接成动态发现触发器</span></h2>
<p data-tool="mdnice编辑器">如果你也想要「技能跟着目录出现」，最小版本就是：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">用户或模型读/写/改文件时</section>
</li>
<li>
<section style="color: #010101;">从文件父目录往上走到 cwd</section>
</li>
<li>
<section style="color: #010101;">看有没有 <code style="color: #0e8aeb;">.agent/skills</code> 或 <code style="color: #0e8aeb;">.claude/skills</code></section>
</li>
<li>
<section style="color: #010101;">找到新目录就加载 skill index</section>
</li>
</ul>
<p data-tool="mdnice编辑器">示意：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">async</span> <span style="color: #c678dd;">function</span> <span style="color: #61aeee;">discoverSkillDirsForFile</span>(filePath: <span style="color: #e6c07b;">string</span>, cwd: <span style="color: #e6c07b;">string</span>): <span style="color: #61aeee;">Promise</span>&lt;<span style="color: #61aeee;">string</span>[]&gt; {
<span style="color: #c678dd;">const</span> dirs: <span style="color: #e6c07b;">string</span>[] = []
<span style="color: #c678dd;">let</span> current = dirname(filePath)
<span style="color: #c678dd;">while</span> (current.startsWith(cwd + sep)) {
    <span style="color: #c678dd;">const</span> candidate = join(current, <span style="color: #98c379;">'.agent'</span>, <span style="color: #98c379;">'skills'</span>)
    <span style="color: #c678dd;">if</span> (<span style="color: #c678dd;">await</span> exists(candidate)) dirs.push(candidate)
    <span style="color: #c678dd;">const</span> parent = dirname(current)
    <span style="color: #c678dd;">if</span> (parent === current) <span style="color: #c678dd;">break</span>
    current = parent
  }
<span style="color: #c678dd;">return</span> dirs
}
</code></pre>
<p data-tool="mdnice编辑器">Claude Code 的现成参考是 [discoverSkillDirsForPaths] skills/loadSkillsDir.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L861</a>-L915。</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">5）第五步：做条件激活，而不是启动时全暴露</span></h2>
<p data-tool="mdnice编辑器">如果 skill 定义里有 <code style="color: #0e8aeb;">paths</code>，就不要一开始暴露。<br />
等碰到匹配文件时再激活：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">function</span> <span style="color: #61aeee;">activatePathScopedSkills</span>(
  pending: Skill[],
  touchedFiles: <span style="color: #e6c07b;">string</span>[],
): { active: Skill[]; remaining: Skill[] } {
<span style="color: #c678dd;">const</span> active: Skill[] = []
<span style="color: #c678dd;">const</span> remaining: Skill[] = []
<span style="color: #c678dd;">for</span> (<span style="color: #c678dd;">const</span> skill of pending) {
    <span style="color: #c678dd;">if</span> (!skill.paths || skill.paths.length === <span style="color: #d19a66;">0</span>) {
      active.push(skill)
      <span style="color: #c678dd;">continue</span>
    }
    <span style="color: #c678dd;">const</span> matched = touchedFiles.some(file =&gt; matchAny(file, skill.paths!))
    <span style="color: #c678dd;">if</span> (matched) active.push(skill)
    <span style="color: #c678dd;">else</span> remaining.push(skill)
  }
<span style="color: #c678dd;">return</span> { active, remaining }
}
</code></pre>
<p data-tool="mdnice编辑器">这就是 Claude Code <code style="color: #0e8aeb;">conditionalSkills -&gt; activateConditionalSkillsForPaths()</code> 的最小复刻。</p>
<hr />
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">6）第六步：调用 skill 时才真正加载正文</span></h2>
<p data-tool="mdnice编辑器">不要提前把 skill 正文塞到 prompt。<br />
调用时再做：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">async</span> <span style="color: #c678dd;">function</span> <span style="color: #61aeee;">invokeSkill</span>(
  skill: Skill,
  args: <span style="color: #e6c07b;">string</span>,
  ctx: AgentContext,
): <span style="color: #61aeee;">Promise</span>&lt;<span style="color: #61aeee;">SkillInvocationResult</span>&gt; {
<span style="color: #c678dd;">const</span> prompt = <span style="color: #c678dd;">await</span> skill.contentLoader(args, ctx)

<span style="color: #c678dd;">if</span> (skill.context === <span style="color: #98c379;">'fork'</span>) {
    <span style="color: #c678dd;">const</span> result = <span style="color: #c678dd;">await</span> runSubAgent({
      prompt,
      allowedTools: skill.allowedTools,
      model: skill.model,
      effort: skill.effort,
    })
    <span style="color: #c678dd;">return</span> { mode: <span style="color: #98c379;">'fork'</span>, result }
  }

<span style="color: #c678dd;">return</span> {
    mode: <span style="color: #98c379;">'inline'</span>,
    newMessages: [
      { role: <span style="color: #98c379;">'user'</span>, content: <span style="color: #98c379;">`[SKILL:<span style="color: #e06c75;">${skill.name}</span>]`</span> },
      { role: <span style="color: #98c379;">'user'</span>, content: prompt, meta: <span style="color: #56b6c2;">true</span> },
    ],
    allowedTools: skill.allowedTools,
    model: skill.model,
    effort: skill.effort,
  }
}
</code></pre>
<p data-tool="mdnice编辑器">这就是 Claude Code <code style="color: #0e8aeb;">SkillTool.call()</code> 的最小骨架。[SkillTool.ts] tools/SkillTool/SkillTool.ts<a class="wx_topic_link" style="color: #576b95 !important;" data-topic="1" data-recommend="">#L581</a>-L863</p>
<h2 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">7）第七步：结果返回必须分 inline 和 fork</span></h2>
<p data-tool="mdnice编辑器">直接照 Claude Code 的语义分两种：</p>
<h3 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;">inline</span></h3>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">返回一个轻 tool_result：<code style="color: #0e8aeb;">Launching skill: xxx</code></section>
</li>
<li>
<section style="color: #010101;">真正内容通过 <code style="color: #0e8aeb;">newMessages</code> 回到主对话继续推理</section>
</li>
</ul>
<h3 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;">fork</span></h3>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">返回最终结果摘要</section>
</li>
<li>
<section style="color: #010101;">子代理对话不污染主上下文</section>
</li>
</ul>
<p data-tool="mdnice编辑器">示意：</p>
<pre data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">type</span> SkillInvocationResult =
  | {
      mode: <span style="color: #98c379;">'inline'</span>
      newMessages: Message[]
      allowedTools?: <span style="color: #e6c07b;">string</span>[]
      model?: <span style="color: #e6c07b;">string</span>
      effort?: <span style="color: #e6c07b;">string</span>
    }
  | {
      mode: <span style="color: #98c379;">'fork'</span>
      result: <span style="color: #e6c07b;">string</span>
    }
</code></pre>
<p data-tool="mdnice编辑器">这一步是很多新 Agent 最容易偷懒的地方。<br />
要么所有 skill 都 inline，主上下文爆炸；要么所有 skill 都 fork，失去细粒度引导。</p>
<h1 data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">九、小结</span></h1>
<p data-tool="mdnice编辑器">「skills 的渐进式披露」其实就是 Claude Code 在控制 prompt 成本和能力密度时最典型的设计之一。它真正解决的问题不是「怎么找到一个 skill」，而是「怎么在不把上下文撑爆的前提下，让模型知道自己有技能可用」。</p>
<p data-tool="mdnice编辑器">它背后的思路：</p>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">先给索引</section>
</li>
<li>
<section style="color: #010101;">再给局部集合</section>
</li>
<li>
<section style="color: #010101;">再给真实正文</section>
</li>
<li>
<section style="color: #010101;">最后才给执行结果</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这是一个很像搜索引擎的设计：摘要、点击、展开、消费，而不是把整本书扔给你。</p>
<p data-tool="mdnice编辑器">以上。</p>
<p>&nbsp;</p>
</section>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/04/claude-code-ai-skills-source/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>深入 Claude Code 源码了解其记忆系统</title>
		<link>https://www.phppan.com/2026/04/claude-code-source-memory/</link>
		<comments>https://www.phppan.com/2026/04/claude-code-source-memory/#comments</comments>
		<pubDate>Sat, 04 Apr 2026 01:19:58 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[ClaudeCode]]></category>
		<category><![CDATA[harness engineering]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2484</guid>
		<description><![CDATA[最近做 Agent 的同学应该大部分都有研读 Claude Code 泄漏的源码，网上出了各种 AI 加持下的 [&#8230;]]]></description>
				<content:encoded><![CDATA[<section id="nice" data-tool="mdnice编辑器" data-website="https://www.mdnice.com">
<section id="nice" style="color: #000000;" data-tool="mdnice编辑器" data-website="https://www.mdnice.com">
<p data-tool="mdnice编辑器">最近做 Agent 的同学应该大部分都有研读 Claude Code 泄漏的源码，网上出了各种 AI 加持下的各种解读，教程，细节分析，甚至包括换了一种语言实现的版本，如 Python，Go，Rust 等等。感觉有点「一鲸落，万物生」的感觉。</p>
<p data-tool="mdnice编辑器">之前学习了 Claude Code 的系统提示词，写了一篇关于记忆系统的提示词。今天我们再深入其源码，看看其实现的细节。</p>
<p data-tool="mdnice编辑器">从其源码来看，</p>
<p data-tool="mdnice编辑器">Claude Code 这套记忆系统把几类完全不同的问题拆开处理了：长期记忆、当前轮相关记忆、会话压缩摘要、子代理独立记忆。和 OpenClaw 不同，OpenClaw 使用了统一的 Memory Service，加上一个向量库做检索，Claude Code 走的是另一条路：<strong style="color: #0e88eb;">文件系统优先，分层清晰，召回时机明确，代价可控</strong>。</p>
<h1 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">1. Claude Code 到底要记了什么</span></h1>
<p data-tool="mdnice编辑器">在 memoryTypes.ts#L14-L31 里，长期记忆的类型是的四类：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">user</code></section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">feedback</code></section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">project</code></section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">reference</code></section>
</li>
</ul>
<p data-tool="mdnice编辑器">这四类东西有一个共同点：它们都<strong style="color: #0e88eb;">不容易从当前代码状态直接推导出来</strong>。用户习惯、项目背景、团队约束、外部系统入口，这些信息不写下来，下次对话就丢了。反过来，代码结构、文件路径、Git 历史、当前临时任务，这些内容源码里明确要求不要进长期记忆，因为它们本来就有权威来源。memoryTypes.ts#L183-L195</p>
<p data-tool="mdnice编辑器">记忆系统只该保存「代码外的信息」和「会跨轮次继续影响决策的信息」。以编程为例，当我们把代码事实也塞进去，后面一定会出现双份真相。你会遇到一个很尴尬的局面：代码说 A，memory 说 B，模型开始摇摆。</p>
<h1 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">2. 四层分工</span></h1>
<p data-tool="mdnice编辑器">第一层是 <code style="color: #0e8aeb;">auto memory / team memory</code>。这是长期记忆，负责跨会话保存信息。目录逻辑在 paths.ts#L79-L259 和 teamMemPaths.ts#L66-L94。</p>
<p data-tool="mdnice编辑器">第二层是 <code style="color: #0e8aeb;">relevant memories</code>。这一层不关心长期存储，它只负责一件事：用户当前这一问，应该把哪几条历史记忆临时塞进上下文。入口在 findRelevantMemories.ts#L39-L141 和 attachments.ts#L2197-L2425。</p>
<p data-tool="mdnice编辑器">第三层是 <code style="color: #0e8aeb;">session memory</code>。这层服务的是长会话压缩，不负责跨会话记忆。位于当前 session 下的 <code style="color: #0e8aeb;">summary.md</code>。sessionMemory.ts#L183-L350</p>
<p data-tool="mdnice编辑器">第四层是 <code style="color: #0e8aeb;">agent memory</code>。子代理如果要持久化自己的经验，可以放 user/project/local 三种 scope 的独立目录。agentMemory.ts#L12-L177</p>
<p data-tool="mdnice编辑器">这四层拆开之后，很多设计选择就顺了：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;">长期记忆用文件，便于审计和手工修复</section>
</li>
<li>
<section style="color: #010101;">当前轮召回走轻量检索，减少 prompt 污染</section>
</li>
<li>
<section style="color: #010101;">长会话压缩用单独 summary，避免每次 compact 都从头总结</section>
</li>
<li>
<section style="color: #010101;">子代理隔离状态，减少串味</section>
</li>
</ul>
<h1 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">3. 长期记忆为什么选文件</span></h1>
<p data-tool="mdnice编辑器">Claude Code 的长期记忆是使用的 Markdown 文件。每条记忆一个文件，外加一个 <code style="color: #0e8aeb;">MEMORY.md</code> 入口索引。这部分规则在 memdir.ts#L199-L316 里写得很明白。</p>
<p data-tool="mdnice编辑器">源码里的写入约束是这样的：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="color: #abb2bf;"><span class="hljs-string" style="color: #98c379;">'## How to save memories'</span>,
<span class="hljs-string" style="color: #98c379;">''</span>,
<span class="hljs-string" style="color: #98c379;">'Saving a memory is a two-step process:'</span>,
<span class="hljs-string" style="color: #98c379;">''</span>,
<span class="hljs-string" style="color: #98c379;">'**Step 1** — write the memory to its own file (e.g., `user_role.md`, `feedback_testing.md`) using this frontmatter format:'</span>,
<span class="hljs-string" style="color: #98c379;">''</span>,
...MEMORY_FRONTMATTER_EXAMPLE,
<span class="hljs-string" style="color: #98c379;">''</span>,
<span class="hljs-string" style="color: #98c379;">`**Step 2** — add a pointer to that file in \`<span class="hljs-subst" style="color: #e06c75;">${ENTRYPOINT_NAME}</span>\`. \`<span class="hljs-subst" style="color: #e06c75;">${ENTRYPOINT_NAME}</span>\` is an index, not a memory — each entry should be one line, under ~150 characters: \`- [Title](file.md) — one-line hook\`. It has no frontmatter. Never write memory content directly into \`<span class="hljs-subst" style="color: #e06c75;">${ENTRYPOINT_NAME}</span>\`.`</span>,
</code></pre>
<p data-tool="mdnice编辑器">实现位置见 memdir.ts#L219-L230。</p>
<p data-tool="mdnice编辑器">还有有三个工程判断。</p>
<p data-tool="mdnice编辑器">第一，<code style="color: #0e8aeb;">MEMORY.md</code> 只是索引，不承载正文。如果把所有记忆都堆到一个大文件里，前期简单，后期灾难。Claude Code 从一开始就做拆分，每条记忆单文件，这样更新一条信息时不会引起全量重写。</p>
<p data-tool="mdnice编辑器">第二，frontmatter 强制有 <code style="color: #0e8aeb;">description</code> 字段，这个字段后面要参与召回。很多团队做知识条目，只写正文，不写检索摘要，最后靠 embedding 硬扛。Claude Code 反过来，它要求记忆写入阶段就产出一条高质量摘要。召回质量在写入那一刻就埋下去了。</p>
<p data-tool="mdnice编辑器">第三，完全基于文件系统，调试成本低。你可以直接去目录里看文件，团队同步时还能走 Git 或远端同步链路。数据库方案最大的问题不在性能，在可观察性。出了问题你要查 schema、查索引、查 embedding 版本、查写入日志，排障很慢。</p>
<p data-tool="mdnice编辑器">文件方案当然也有代价。文件一多，目录扫描成本会上升；<code style="color: #0e8aeb;">MEMORY.md</code> 入口过长也会逼近 prompt token 上限。Claude Code 后面靠动态召回机制兜住了这个问题，这个设计是连起来看的。</p>
<h1 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">4. 写入链路</span></h1>
<p data-tool="mdnice编辑器">它怎么把记忆真正落盘</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">4.1 主模型直接写</span></h2>
<p data-tool="mdnice编辑器">长期记忆的第一条写入链路，是主模型自己写。系统 prompt 里已经告诉它记忆目录在哪、允许写什么、怎么写文件、什么时候写。</p>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">loadMemoryPrompt()</code> 会把 memory rules 注入系统提示词，入口在 [loadMemoryPrompt] memdir.ts#L419-L507。这一段 prompt 并没有替模型做决策，它只是把写入协议放进脑子里：目录、类型、索引格式、读取时机、失效校验。</p>
<p data-tool="mdnice编辑器">这意味着 Claude Code 对模型的假设很明确：模型可以自己判断「这条信息值不值得保存」，然后调用写文件工具去落盘。写入不是一个外置 API，写入就是普通文件操作。</p>
<p data-tool="mdnice编辑器">这条路有个好处：反馈延迟很低。用户刚说完「记住这个偏好」，主模型当轮就能写，不用等后台任务。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">4.2 后台抽取器补写</span></h2>
<p data-tool="mdnice编辑器">如果主模型这一轮没动手写，系统会在 turn end 触发后台抽取器。stop hook 在 stopHooks.ts#L141-L156：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="color: #abb2bf;"><span class="hljs-keyword" style="color: #c678dd;">if</span> (
  feature(<span class="hljs-string" style="color: #98c379;">'EXTRACT_MEMORIES'</span>) &amp;&amp;
  !toolUseContext.agentId &amp;&amp;
  isExtractModeActive()
) {
  <span class="hljs-built_in" style="color: #e6c07b;">void</span> extractMemoriesModule!.executeExtractMemories(
    stopHookContext,
    toolUseContext.appendSystemMessage,
  )
}
</code></pre>
<p data-tool="mdnice编辑器">真正逻辑在 extractMemories.ts#L329-L567。</p>
<p data-tool="mdnice编辑器">这条链路里最重要的一段判断是：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="color: #abb2bf;"><span class="hljs-keyword" style="color: #c678dd;">if</span> (hasMemoryWritesSince(messages, lastMemoryMessageUuid)) {
  logForDebugging(
    <span class="hljs-string" style="color: #98c379;">'[extractMemories] skipping — conversation already wrote to memory files'</span>,
  )
  ...
  <span class="hljs-keyword" style="color: #c678dd;">return</span>
}
</code></pre>
<p data-tool="mdnice编辑器">位置见 extractMemories.ts#L345-L360。</p>
<p data-tool="mdnice编辑器">它防的是双写。主模型已经写过，后台抽取器就别再重做一遍。很多系统做异步归档时忘了这件事，最后要么生成重复记忆，要么覆盖用户刚刚确认的内容。</p>
<p data-tool="mdnice编辑器">后台抽取器的权限也非常收敛。<code style="color: #0e8aeb;">createAutoMemCanUseTool()</code> 明确规定，只准：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;">Read / Grep / Glob</section>
</li>
<li>
<section style="color: #010101;">只读 Bash</section>
</li>
<li>
<section style="color: #010101;">memory 目录内的 Edit / Write</section>
</li>
</ul>
<p data-tool="mdnice编辑器">实现见 [createAutoMemCanUseTool] extractMemories.ts#L166-L222。</p>
<p data-tool="mdnice编辑器">extractor 的职责：它只做归档，不许顺手验证代码，不许顺手修改业务文件，不许借机跑工具链。权限如果不锁死，后台代理迟早会从归档器膨胀成第二个主代理。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">4.3 KAIROS 下的写法</span></h2>
<p data-tool="mdnice编辑器">KAIROS 模式更有意思。它不要求模型实时维护 <code style="color: #0e8aeb;">MEMORY.md</code>，新记忆先按天追加到日志文件里。规则在 [buildAssistantDailyLogPrompt] memdir.ts#L318-L370。</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="color: #abb2bf;"><span class="hljs-string" style="color: #98c379;">"This session is long-lived. As you work, record anything worth remembering by **appending** to today's daily log file:"</span>,
<span class="hljs-string" style="color: #98c379;">` \`<span class="hljs-subst" style="color: #e06c75;">${logPathPattern}</span>\` `</span>,
<span class="hljs-string" style="color: #98c379;">'Write each entry as a short timestamped bullet. Create the file (and parent directories) on first write if it does not exist. Do not rewrite or reorganize the log — it is append-only. A separate nightly process distills these logs into `MEMORY.md` and topic files.'</span>,
</code></pre>
<p data-tool="mdnice编辑器">这条策略很适合长驻 Agent。会话存活时间长时，频繁重写 topic files 和索引很贵，冲突也多。先写 append-only 日志，夜间再蒸馏，吞吐更稳，模型也更不容易在白天工作时把记忆目录写乱。</p>
<h1 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">5. 召回链路</span></h1>
<p data-tool="mdnice编辑器">它怎么决定哪段记忆该进来</p>
<p data-tool="mdnice编辑器">Claude Code 的召回要分成两种看。</p>
<p data-tool="mdnice编辑器">一种是静态注入，也就是固定随上下文加载的那些东西。另一种是动态召回，根据当前 query 临时挑选最相关的记忆文件。</p>
<p data-tool="mdnice编辑器">很多系统只做前者，结果上下文越来越肥。很多系统只做后者，结果基本行为约束丢了。Claude Code 两条都做。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">5.1 静态注入</span></h2>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">getUserContext()</code> 会构造一个 <code style="color: #0e8aeb;">claudeMd</code> 字段，位置在 context.ts#L155-L188。</p>
<p data-tool="mdnice编辑器">核心调用是：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="color: #abb2bf;"><span class="hljs-keyword" style="color: #c678dd;">const</span> claudeMd = shouldDisableClaudeMd
  ? <span class="hljs-literal" style="color: #56b6c2;">null</span>
  : getClaudeMds(filterInjectedMemoryFiles(<span class="hljs-keyword" style="color: #c678dd;">await</span> getMemoryFiles()))
</code></pre>
<p data-tool="mdnice编辑器"><code style="color: #0e8aeb;">getMemoryFiles()</code> 的实现很长，在 claudemd.ts#L790-L1075。它会统一加载：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;">Managed 指令</section>
</li>
<li>
<section style="color: #010101;">User 指令</section>
</li>
<li>
<section style="color: #010101;">Project 指令</section>
</li>
<li>
<section style="color: #010101;">Local 指令</section>
</li>
<li>
<section style="color: #010101;">AutoMem 的 <code style="color: #0e8aeb;">MEMORY.md</code></section>
</li>
<li>
<section style="color: #010101;">TeamMem 的 <code style="color: #0e8aeb;">MEMORY.md</code></section>
</li>
</ul>
<p data-tool="mdnice编辑器">然后 <code style="color: #0e8aeb;">getClaudeMds()</code> 把这些文件串成提示词内容，[getClaudeMds] claudemd.ts#L1153-L1195。</p>
<p data-tool="mdnice编辑器">它给模型一个稳定的全局工作框架。它会知道项目规则、用户偏好、团队共享记忆索引。它适合放那些「大方向会持续生效」的内容。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">5.2 动态召回</span></h2>
<p data-tool="mdnice编辑器">静态注入解决不了所有问题。长期记忆正文一多，全部塞进 prompt 代价太高。Claude Code 的处理方式，是每轮用户发言后启动一个相关记忆预取。</p>
<p data-tool="mdnice编辑器">入口在 query.ts#L297-L304：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="color: #abb2bf;">using pendingMemoryPrefetch = startRelevantMemoryPrefetch(
  state.messages,
  state.toolUseContext,
)
</code></pre>
<p data-tool="mdnice编辑器">这个预取不会阻塞主流程。到后面条件满足时再消费，query.ts#L1595-L1617。</p>
<p data-tool="mdnice编辑器">真正检索逻辑在 attachments.ts#L2197-L2425。它会先决定搜索哪个目录：如果用户显式提到某个 agent，就搜 agent memory；否则搜 auto memory。</p>
<p data-tool="mdnice编辑器">然后调用 [findRelevantMemories] findRelevantMemories.ts#L39-L141。</p>
<p data-tool="mdnice编辑器">这里最有意思的点在于，它没有用向量库。它的步骤是：</p>
<ol data-tool="mdnice编辑器">
<li>
<section style="color: #010101;">
<p style="color: #000000;"><code style="color: #0e8aeb;">scanMemoryFiles()</code> 扫 memory 目录里的 <code style="color: #0e8aeb;">.md</code> 文件，读 frontmatter，产出一个 manifest<br />
见 memoryScan.ts#L35-L94</p>
</section>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;">把 <code style="color: #0e8aeb;">用户 query + manifest</code> 发给一个 sideQuery 模型<br />
见 findRelevantMemories.ts#L77-L141</p>
</section>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;">让这个模型返回最多 5 个文件名</p>
</section>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;">再去读取这些文件正文，截断到限定行数和字节数<br />
见 [readMemoriesForSurfacing] attachments.ts#L2280-L2333</p>
</section>
</li>
</ol>
<p data-tool="mdnice编辑器">这套方案的好处：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;">没有 embedding 构建成本</section>
</li>
<li>
<section style="color: #010101;">没有索引维护复杂度</section>
</li>
<li>
<section style="color: #010101;">manifest 很小，side query 很快</section>
</li>
<li>
<section style="color: #010101;">召回逻辑对开发者可见，容易调</section>
</li>
</ul>
<p data-tool="mdnice编辑器">缺点也明确。召回质量强依赖 frontmatter 的 <code style="color: #0e8aeb;">description</code>。写入时 description 写差了，后面召回一定差。</p>
<h1 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">6. 记忆怎么进入上下文</span></h1>
<p data-tool="mdnice编辑器">不是一处注入，是四处入口</p>
<p data-tool="mdnice编辑器">很多人看 Agent 源码时老在问「上下文是在什么地方拼进去的」。这个问题本身就有误导性。Claude Code 里记忆的注入入口不止一个。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">6.1 system prompt 入口</span></h2>
<p data-tool="mdnice编辑器">第一处是 system prompt。这里进来的内容主要是「记忆系统的使用规则」，比如什么时候读、什么时候存、什么时候验证失效。对应实现是 prompts.ts#L492-L527 调 <code style="color: #0e8aeb;">loadMemoryPrompt()</code>。</p>
<p data-tool="mdnice编辑器">这是行为层指令。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">6.2 user context 入口</span></h2>
<p data-tool="mdnice编辑器">第二处是 <code style="color: #0e8aeb;">getUserContext()</code> 构造的 <code style="color: #0e8aeb;">claudeMd</code>。这里进来的是 <code style="color: #0e8aeb;">CLAUDE.md</code>、rules、<code style="color: #0e8aeb;">MEMORY.md</code> 这种比较稳定的文本。context.ts#L155-L188</p>
<p data-tool="mdnice编辑器">这是稳定背景层。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">6.3 attachment 入口</span></h2>
<p data-tool="mdnice编辑器">第三处是 relevant memory attachment。被召回的正文不会直接拼到 <code style="color: #0e8aeb;">claudeMd</code>，而是先变成 attachment，再由 messages.ts#L3743-L3756 包装成 <code style="color: #0e8aeb;">&lt;system-reminder&gt;</code>。</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs" style="color: #abb2bf;"><span class="hljs-keyword" style="color: #c678dd;">return</span> wrapMessagesInSystemReminder(
  attachment.memories.map(<span class="hljs-function"><span class="hljs-params">m</span> =&gt;</span> {
    <span class="hljs-keyword" style="color: #c678dd;">const</span> header = m.header ?? memoryHeader(m.path, m.mtimeMs)
    <span class="hljs-keyword" style="color: #c678dd;">return</span> createUserMessage({
      content: <span class="hljs-string" style="color: #98c379;">`<span class="hljs-subst" style="color: #e06c75;">${header}</span>\n\n<span class="hljs-subst" style="color: #e06c75;">${m.content}</span>`</span>,
      isMeta: <span class="hljs-literal" style="color: #56b6c2;">true</span>,
    })
  }),
)
</code></pre>
<p data-tool="mdnice编辑器">这意味着这些记忆是临时的、按轮次加载的、带 freshness header 的系统提醒。它的优先级和普通用户消息不同。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">6.4 compact summary 入口</span></h2>
<p data-tool="mdnice编辑器">第四处是 session memory compact。上下文过长后，系统会把会话前半段替换为一条 summary message，summary 内容来自 <code style="color: #0e8aeb;">summary.md</code> 的裁剪版。sessionMemoryCompact.ts#L437-L503</p>
<p data-tool="mdnice编辑器">这是上下文续命层。</p>
<p data-tool="mdnice编辑器">四处入口分工以后，就能看明白为什么 Claude Code 的行为相对稳定：规则、稳定背景、临时相关信息、压缩摘要，各走各的通道，互相不抢角色。</p>
<h1 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">7. 真正的压缩发生在哪里</span></h1>
<p data-tool="mdnice编辑器">很多人一听「记忆系统」，第一反应是长期 memory 压缩。Claude Code 里最成熟的压缩逻辑，实际上落在 session memory 上。</p>
<p data-tool="mdnice编辑器">前面说过，session memory 是 <code style="color: #0e8aeb;">summary.md</code>，它本身就是会话结构化摘要。维护逻辑在 sessionMemory.ts#L272-L350。</p>
<p data-tool="mdnice编辑器">当上下文真的不够时，系统优先尝试 <code style="color: #0e8aeb;">trySessionMemoryCompaction()</code>，sessionMemoryCompact.ts#L514-L619。</p>
<p data-tool="mdnice编辑器">它的动作顺序：</p>
<ol data-tool="mdnice编辑器">
<li>
<section style="color: #010101;">先确认 session memory 功能和 compact 功能都开着</section>
</li>
<li>
<section style="color: #010101;">等待正在进行中的 session memory 抽取结束</section>
</li>
<li>
<section style="color: #010101;">读取 <code style="color: #0e8aeb;">summary.md</code></section>
</li>
<li>
<section style="color: #010101;">如果还是空模板，放弃，退回传统 compact</section>
</li>
<li>
<section style="color: #010101;">计算需要保留的 recent messages 窗口</section>
</li>
<li>
<section style="color: #010101;">用 <code style="color: #0e8aeb;">summary.md</code> 的裁剪版构造 compact summary</section>
</li>
<li>
<section style="color: #010101;">组装 <code style="color: #0e8aeb;">boundary + summary + recent messages + attachments + hooks</code></section>
</li>
</ol>
<p data-tool="mdnice编辑器">这里的「保留 recent messages」特别关键。作者没有图省事把所有旧消息都抹掉，而是保留一段最近窗口。窗口大小由 [DEFAULT_SM_COMPACT_CONFIG] sessionMemoryCompact.ts#L56-L66 定义，默认：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">minTokens = 10000</code></section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">minTextBlockMessages = 5</code></section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">maxTokens = 40000</code></section>
</li>
</ul>
<p data-tool="mdnice编辑器">保留窗口的计算在 [calculateMessagesToKeepIndex] sessionMemoryCompact.ts#L324-L397。</p>
<p data-tool="mdnice编辑器">这个策略解决的问题是：<strong style="color: #0e88eb;">摘要永远会损失细节</strong>，最近一段工作现场最好保留原始消息，模型续做时不至于失真。要是所有内容都只剩 summary，模型会失去工具调用上下文、局部错误信息、最近的计划变更。</p>
<p data-tool="mdnice编辑器">更细的一层防御在 <code style="color: #0e8aeb;">adjustIndexToPreserveAPIInvariants()</code>。 sessionMemoryCompact.ts#L232-L314</p>
<p data-tool="mdnice编辑器">它干的事情很硬核，也很必要：如果最近保留窗口里出现了 <code style="color: #0e8aeb;">tool_result</code>，系统必须把匹配的 <code style="color: #0e8aeb;">tool_use</code> 也补进来；如果 assistant 消息因为流式输出被拆成多个共享 <code style="color: #0e8aeb;">message.id</code> 的块，thinking 和 tool_use 也要一起补齐。否则 compact 后发给 API 的消息链会断，直接报错。</p>
<p data-tool="mdnice编辑器">这一段代码说明作者踩过坑，或者至少认真想过 API 侧不变量。很多开源 Agent 框架在消息压缩这里写得很草，最后线上 bug 都长一个样：tool result 找不到 parent，thinking 丢了，message 合并失败。</p>
<h1 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">8. session memory 自己也会被裁剪</span></h1>
<p data-tool="mdnice编辑器">就算 <code style="color: #0e8aeb;">summary.md</code> 已经是摘要，compact 时还会再做一次 section 级裁剪。逻辑在 [truncateSessionMemoryForCompact] prompts.ts#L249-L295。</p>
<p data-tool="mdnice编辑器">过程如下：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;">按 <code style="color: #0e8aeb;"># section</code> 拆段</section>
</li>
<li>
<section style="color: #010101;">每个 section 允许的大小用 <code style="color: #0e8aeb;">MAX_SECTION_LENGTH * 4</code> 粗略换算成字符数</section>
</li>
<li>
<section style="color: #010101;">超过就按行保留前半部分</section>
</li>
<li>
<section style="color: #010101;">最后插入 <code style="color: #0e8aeb;">[... section truncated for length ...]</code></section>
</li>
</ul>
<p data-tool="mdnice编辑器">实际截断函数见 [flushSessionSection] prompts.ts#L298-L324。</p>
<p data-tool="mdnice编辑器">这套逻辑谈不上优雅，语义理解也谈不上深入，但它有一个优点：非常稳。系统真的到了上下文极限时，保底截断总比把整个 compact 失败掉强。工程里很多时候要的是「退化可接受」，不是「完美压缩」。</p>
<p data-tool="mdnice编辑器">然后 <code style="color: #0e8aeb;">createCompactionResultFromSessionMemory()</code> 把裁剪后的 session memory 包成 summary message。 sessionMemoryCompact.ts#L437-L503</p>
<p data-tool="mdnice编辑器">这里还有一个细节：如果发生过裁剪，它会额外附一句话，告诉模型和人类完整 session memory 文件路径在哪。排障时你可以直接打开原始 <code style="color: #0e8aeb;">summary.md</code>，不用猜裁掉了什么。</p>
<h1 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">9. KAIROS 的压缩逻辑和普通模式不一样</span></h1>
<p data-tool="mdnice编辑器">KAIROS 里还有另一种「压缩」，它压的不是当前上下文，而是长期事件流。</p>
<p data-tool="mdnice编辑器">在 memdir.ts#L321-L349 里能看到，KAIROS 模式下白天写的是 append-only daily log。到夜间，<code style="color: #0e8aeb;">/dream</code> 流程会把这些日志蒸馏成 topic files 和 <code style="color: #0e8aeb;">MEMORY.md</code>。</p>
<p data-tool="mdnice编辑器">这是另一类压缩：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;">输入是时间顺序日志</section>
</li>
<li>
<section style="color: #010101;">输出是主题化长期记忆</section>
</li>
</ul>
<p data-tool="mdnice编辑器">session memory compact 处理的是「上下文窗口」问题。KAIROS dream 处理的是「长期事件沉淀」问题。这两类压缩混在一起看会非常乱，源码里其实已经把它们分得很开。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">10 小结</span></h2>
<p data-tool="mdnice编辑器">这套设计的工程代价与收益</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">10.1 一些值得学习的点</span></h2>
<p data-tool="mdnice编辑器">第一，分层彻底。长期记忆、当前轮召回、会话摘要、子代理记忆，各自有自己的存储形态和注入入口。系统复杂度是被隔离开的。</p>
<p data-tool="mdnice编辑器">第二，文件优先。排查方便，审计方便，人工纠错方便。很多团队高估了数据库和向量库的必要性，低估了可观察性的重要性。</p>
<p data-tool="mdnice编辑器">第三，动态召回走轻量 manifest + side query。对 CLI Agent 这种高频交互场景，这个方案的性价比很高。它把复杂度留给模型的小规模选择，而不是重型检索基础设施。</p>
<p data-tool="mdnice编辑器">第四，压缩时保 recent window，并修补 tool_use/tool_result 不变量。这一点极少有团队一开始就写对。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">10.2 一些代价</span></h2>
<p data-tool="mdnice编辑器">第一，frontmatter 的 <code style="color: #0e8aeb;">description</code> 质量变成关键依赖。这个字段一旦写烂，召回效果会大幅波动。它省掉了 embedding 的复杂度，也把一部分压力前置给写入质量。</p>
<p data-tool="mdnice编辑器">第二，双通道写入意味着状态机会更复杂。主模型可以写，后台 extractor 也能写。虽然代码里有跳过逻辑，但这类架构天然比单通道更需要小心。</p>
<p data-tool="mdnice编辑器">第三，session memory 的 section 截断是粗粒度的。它靠字符数近似 token，再按行截断，这属于保底工程，不属于精细压缩。能用，谈不上漂亮。</p>
<p data-tool="mdnice编辑器">第四，<code style="color: #0e8aeb;">MEMORY.md</code> 仍然有索引容量压力。即便动态召回已经分担了很大一部分负担，入口索引的组织质量依然重要。</p>
<h2 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">10.3 我们能用什么</span></h2>
<p data-tool="mdnice编辑器">如果把这套思路迁移到我们自己的 Agent 系统，可以借鉴（抄）：</p>
<p data-tool="mdnice编辑器">第一，先拆问题，再选技术。你要先决定自己在解哪件事：跨会话长期记忆、当前轮检索、超长对话压缩、团队共享经验。不要一上来就建一个统一 Memory API。</p>
<p data-tool="mdnice编辑器">第二，先用文件，再考虑数据库。只要你的系统规模还没逼到那个份上，文件系统几乎总是更划算。它便宜、透明、好调试。很多团队用数据库，是因为觉得那样「更像正经系统」，这个判断没什么含金量。</p>
<p data-tool="mdnice编辑器">第三，把召回质量的责任前移到写入阶段。Claude Code 用 <code style="color: #0e8aeb;">description</code> 做 manifest 检索这件事，给我的启发很大。与其指望后面靠复杂召回算法弥补，不如要求写入时就产出高质量摘要和类型信息。</p>
<p data-tool="mdnice编辑器">如果你们团队正在做本地化的企业内 Agent，我甚至建议先抄一版这种架构原型：<br />
长期记忆用 Markdown + frontmatter，当前轮召回走 manifest + 小模型筛选，会话压缩单独维护结构化 summary。三周内就能跑起来。比起一开始堆向量库、事件总线、关系数据库，这条路短得多。</p>
<h1 data-tool="mdnice编辑器"><span class="content" style="color: #0e8aeb;">11. 其它</span></h1>
<p data-tool="mdnice编辑器">Claude Code 的记忆系统没有神秘技术。它的难点不在某个单点算法，在边界控制和时机设计。什么时候写，写到哪里，什么时候读，读多少，压缩后保留什么，这些问题都比「用什么模型做召回」更重要。</p>
<p data-tool="mdnice编辑器">如果你把这篇文章里的结论压成一句工程建议，那就是：<br />
先把长期记忆、短期相关记忆、会话压缩拆开，再谈检索和存储。</p>
<p data-tool="mdnice编辑器">源码入口可以优先读这几组文件：</p>
<ul data-tool="mdnice编辑器">
<li>
<section style="color: #010101;">
<p style="color: #000000;">长期记忆规则与路径：<br />
[memdir.ts] [paths.ts] [memoryTypes.ts]</p>
</section>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;">记忆静态注入与动态召回：<br />
[claudemd.ts] [attachments.ts] [findRelevantMemories.ts]</p>
</section>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;">会话记忆与压缩：<br />
[sessionMemory.ts] [prompts.ts] [sessionMemoryCompact.ts]</p>
</section>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;">团队共享记忆：<br />
[teamMemPaths.ts] [teamMemorySync/index.ts]</p>
</section>
</li>
</ul>
<p data-tool="mdnice编辑器">以上。</p>
</section>
</section>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/04/claude-code-source-memory/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>聊下 OpenClaw 的记忆系统</title>
		<link>https://www.phppan.com/2026/03/openclaw-memory/</link>
		<comments>https://www.phppan.com/2026/03/openclaw-memory/#comments</comments>
		<pubDate>Sun, 15 Mar 2026 11:37:01 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[Memory System]]></category>
		<category><![CDATA[OpenClaw]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2477</guid>
		<description><![CDATA[OpenClaw 是最近 AI 圈最火的一个开源项目，没有之一。 从去年的 Agent 年，到今年的 AI 个 [&#8230;]]]></description>
				<content:encoded><![CDATA[<section id="nice" data-tool="mdnice编辑器" data-website="https://www.mdnice.com">
<p data-tool="mdnice编辑器">OpenClaw 是最近 AI 圈最火的一个开源项目，没有之一。</p>
<p data-tool="mdnice编辑器">从去年的 Agent 年，到今年的 AI 个人助理，OpenClaw 和去年 Manus 一样的，爆到不行，而且还是开源的版本。</p>
<p data-tool="mdnice编辑器">由于最近自己也在做 Agent，于是也看了 OpenClaw 的代码来了解其记忆系统的实现。有一些觉得可以借鉴学习的地方。</p>
<p data-tool="mdnice编辑器">OpenClaw 的记忆系统其实比较简单：它把「记忆」拆成了<strong>文件</strong>、<strong>索引</strong>、<strong>召回注入</strong>。</p>
<h1 data-tool="mdnice编辑器"><span class="content">1. 「Agent 记忆系统」的定义</span></h1>
<p data-tool="mdnice编辑器">在 Agent 工程里，记忆是一套能力组合：</p>
<ol data-tool="mdnice编辑器">
<li>
<section><strong>持久化</strong>：跨会话保存事实、偏好、决策、未完成事项。</section>
</li>
<li>
<section><strong>可检索</strong>：能在需要时把相关片段拉出来，且可控预算。</section>
</li>
<li>
<section><strong>可注入</strong>：把召回结果以确定的结构进入模型上下文，不靠「它自己想起来」。</section>
</li>
<li>
<section><strong>可审计</strong>：出了错能定位「写入发生在什么时候」「召回命中了什么」「注入了哪些行」。</section>
</li>
<li>
<section><strong>可治理</strong>：能处理泄露风险、过期信息、重复信息、冲突信息。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">OpenClaw 的实现路径非常「工程」：<strong>Markdown 作为事实源</strong>，<strong>SQLite 作为检索索引</strong>，<strong>toolResult 作为注入通道</strong>。</p>
<h1 data-tool="mdnice编辑器"><span class="content">2. OpenClaw 的三层记忆</span></h1>
<p data-tool="mdnice编辑器">OpenClaw 的记忆从存储逻辑上来看可以分为三层：</p>
<h2 data-tool="mdnice编辑器"><span class="content">2.1 会话记忆</span></h2>
<ul data-tool="mdnice编辑器">
<li>
<section><strong>介质</strong>：内存为主，但 OpenClaw 会把 session 打印成类似日志的文件，放到 <code>sessions</code> 目录。</section>
</li>
<li>
<section><strong>内容</strong>：用户消息、OpenClaw 的思考过程、工具调用、skill 调用、最终回复。</section>
</li>
<li>
<section><strong>边界</strong>：会话结束后「可用性」就不可靠了。你能在文件里回放，但模型下一次对话并不会天然带着它。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">会话记忆更像「trace」。我们不能指望它解决跨会话连续性，只用它做排障、复盘、抽取素材（写入短期/长期）。</p>
<h2 data-tool="mdnice编辑器"><span class="content">2.2 短期记忆</span></h2>
<ul data-tool="mdnice编辑器">
<li>
<section><strong>介质</strong>：磁盘，<code>memory/YYYY-MM-DD.md</code> 为主（参考内容给了例子 <code>2026-03-10.md</code>）。</section>
</li>
<li>
<section><strong>内容</strong>：当天重要事件、过程笔记、TODO。关键点是「重要性」由人设与调教决定。</section>
</li>
<li>
<section><strong>边界</strong>：短期记忆是<strong>追加式日志</strong>，质量会漂移。写得越多，噪声越大；但写得太少，又召回不到。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">短期记忆适合承接「会话压缩之前的落盘」和「跨几天的上下文连续性」。它不是最终事实源，别把它当永久协议文档。</p>
<h2 data-tool="mdnice编辑器"><span class="content">2.3 长期记忆</span></h2>
<ul data-tool="mdnice编辑器">
<li>
<section><strong>介质</strong>：工作区根目录 <code>MEMORY.md</code>（参考内容明确）。</section>
</li>
<li>
<section><strong>内容</strong>：核心认知与关系、偏好风格、长期目标、进行中任务、关键事件/教训/决策。</section>
</li>
<li>
<section><strong>边界</strong>：参考内容强调「只在主会话加载」，群聊等任务不加载，避免泄露。</section>
</li>
</ul>
<p data-tool="mdnice编辑器"><code>MEMORY.md</code> ==「可执行的组织记忆」。它的价值不在于「写得多」，在于「冲突少、可被召回、能约束后续行为」。这层要治理，要像维护配置一样维护。</p>
<h1 data-tool="mdnice编辑器"><span class="content">3. OpenClaw 的文件布局</span></h1>
<h2 data-tool="mdnice编辑器"><span class="content">3.1 「会话快照」文件</span></h2>
<p data-tool="mdnice编辑器">快照文件主要是解决 <code>/new</code>、<code>/reset</code> 指令的断片</p>
<p data-tool="mdnice编辑器">session-memory 的 Hook 会在你执行 <code>/new</code> 或 <code>/reset</code> 前，把上一会话最近 N 条对话（默认 15 条）抽出来，写成一个 Markdown 文件放到 <code>workspace/memory/</code> 下。</p>
<ul data-tool="mdnice编辑器">
<li>
<section>命名：<code>YYYY-MM-DD-&lt;slug&gt;.md</code>，slug 通常由 LLM 根据主题生成；LLM 不可用就回退成 <code>HHMM</code>。</section>
</li>
<li>
<section>关键点：这种文件也会被检索索引到。原因是 <code>listMemoryFiles()</code> 会递归扫描 <code>workspace/memory/</code> 下所有 <code>.md</code>，并不要求必须是 <code>YYYY-MM-DD.md</code>（ <code>src/memory/internal.ts:115-145</code> ）。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这个机制对「人类工作流」很友好。很多团队的真实使用是：今天临时开了个话题，明天又忘了开在另一个会话里。会话快照能把碎片变成可检索素材，后面再沉淀进 <code>MEMORY.md</code>。</p>
<h2 data-tool="mdnice编辑器"><span class="content">3.2 <code>memory/main.sqlite</code>：索引库</span></h2>
<ul data-tool="mdnice编辑器">
<li>
<section><code>memory/main.sqlite</code> 基本可以确定是「记忆搜索（memory_search）」用的 SQLite 索引库。</section>
</li>
<li>
<section>索引对象：<code>MEMORY.md</code>、<code>memory/**/*.md</code>，以及你配置的 <code>extraPaths</code>，可选 session transcripts。</section>
</li>
<li>
<section>检索方式：FTS/BM25 关键词检索 +（可选）向量相似度检索（sqlite-vec）。</section>
</li>
<li>
<section>它存的典型结构：<code>files</code>、<code>chunks</code>、<code>embedding_cache</code>，以及可选的 FTS 表、向量虚表。</section>
</li>
</ul>
<p data-tool="mdnice编辑器"><strong>事实源是文本文件</strong>，<strong>索引是可重建的派生物</strong>。索引坏了你删掉重建就行；事实源坏了才是真的坏。</p>
<h1 data-tool="mdnice编辑器"><span class="content">4. 怎么建索引：</span></h1>
<p data-tool="mdnice编辑器">建索引我们关心的三件事：增量、去重、成本</p>
<p data-tool="mdnice编辑器">OpenClaw 的索引构建不是「每次全量重算」，也不是「精细 diff」：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><strong>更新粒度</strong>：按文件做增量。文件没变直接跳过。</section>
</li>
<li>
<section><strong>分块粒度</strong>：文件变了就重建该文件 chunks。</section>
</li>
<li>
<section><strong>向量成本</strong>：chunk embedding 通过缓存复用，避免重复调用 embedding provider。</section>
</li>
</ul>
<h2 data-tool="mdnice编辑器"><span class="content">4.1 扫描哪些文件会进索引</span></h2>
<p data-tool="mdnice编辑器">OpenClaw 会递归扫描 <code>workspace/memory/</code> 下所有 <code>.md</code>，并包含 <code>MEMORY.md</code>/<code>memory.md</code>。对应定位是 <code>src/memory/internal.ts:115-145</code>。</p>
<h2 data-tool="mdnice编辑器"><span class="content">4.2 文件级 hash：没变就跳过</span></h2>
<p data-tool="mdnice编辑器">文件 hash 的策略：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>Markdown：<code>hash = sha256(content)</code>（<code>internal.ts:L245-L263</code>）</section>
</li>
<li>
<section>多模态：buffer 也会 hash，最后把 <code>{path, contentText, mimeType, dataHash}</code> 做 JSON 再 sha256（<code>internal.ts:L204-L243</code>）</section>
</li>
<li>
<section>增量判定：对比 <code>files</code> 表里的 hash，一致就跳过（参考内容列了 memory 文件与 session 文件两条路径）。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">hash 是增量的核心。它的意义不止省时间，还省钱：embedding provider 往往是计费点。这种主要是对于使用第三方 embedding 的。</p>
<h2 data-tool="mdnice编辑器"><span class="content">4.3 分块策略：<code>tokens*4</code> 的字符近似</span></h2>
<p data-tool="mdnice编辑器"><code>chunkMarkdown()</code> 的策略：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><code>maxChars = tokens * 4</code>，<code>overlapChars = overlap * 4</code></section>
</li>
<li>
<section>以「行」为主，超长行会被切段</section>
</li>
<li>
<section>每块生成 <code>hash=sha256(text)</code>，并有 <code>embeddingInput</code></section>
</li>
</ul>
<p data-tool="mdnice编辑器"><code>tokens*4</code> 这种近似在工程里挺常见，优点是简单、稳定、跨模型大差不差。缺点也明显：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>语言差异会影响 token/char 比例；中英文混排时 chunk 尺寸会漂。</section>
</li>
<li>
<section>以行切块对 Markdown 友好，但对「一行很长的 JSON 或日志」不友好。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">可以盯两个指标来调 chunking：</p>
<ol data-tool="mdnice编辑器">
<li>
<section>平均召回 snippet 的「可读性」和「自洽性」；</section>
</li>
<li>
<section>SQLite 体积与索引更新耗时。chunk 太小召回碎，太大注入贵。</section>
</li>
</ol>
<h2 data-tool="mdnice编辑器"><span class="content">4.4 chunk 的唯一标识与 upsert</span></h2>
<p data-tool="mdnice编辑器">去重靠 id，chunk 写入策略如下：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>chunk <code>id</code>：<code>sha256("${source}:${path}:${startLine}:${endLine}:${chunk.hash}:${provider.model}")</code></section>
</li>
<li>
<section>同一 id：<code>ON CONFLICT(id) DO UPDATE</code> 覆盖更新</section>
</li>
<li>
<section>文件要重建时会先清旧再写新（参考内容总结了「清旧再写新」语义）</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这里有个很实际的 trade-off：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>它不做「chunk diff」，所以文件变了就重建该文件 chunks，逻辑简单，坏处是 IO 多。</section>
</li>
<li>
<section>但 embedding 通过缓存复用，把最贵的部分压下去了。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">另外，<code>id</code> 里带了 <code>provider.model</code>，这会带来一个工程后果：<strong>embedding 模型换了，chunk id 会变</strong>，索引层面等价于全量重建。 <code>provider/model/providerKey</code> 变化会触发 full reindex。</p>
<h2 data-tool="mdnice编辑器"><span class="content">4.5 embedding_cache</span></h2>
<p data-tool="mdnice编辑器">embedding 缓存的主键设计：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>主键：<code>(provider, model, provider_key, hash)</code></section>
</li>
<li>
<section><code>provider_key</code> 会把 endpoint/headers 等纳入指纹，避免跨配置污染缓存（而且会剔除授权头的细节在参考内容里提到）。</section>
</li>
<li>
<section>批量加载命中就跳过 embed；miss 才请求，成功回写缓存（对应 <code>manager-embedding-ops.ts</code> 的行段）。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这是「上线能用」的关键。否则就会遇到一个很尴尬的情况：<br />
索引更新频繁触发 embedding 重算 → 延迟抖动 → API 费暴涨 → 还可能被 provider 限流。 system prompt 的 skills 段落甚至提醒「假设有 rate limits，避免 tight loop」，这就有点被打过之后写进规范的味道。</p>
<h1 data-tool="mdnice编辑器"><span class="content">5. 更新怎么触发</span></h1>
<p data-tool="mdnice编辑器">watch、interval、onSearch、session-delta</p>
<p data-tool="mdnice编辑器">索引更新如果做得「过勤」，会把 CPU 和 IO 吃满；做得「过懒」，召回就是过期的。OpenClaw 在触发上给了多条路径：</p>
<ol data-tool="mdnice编辑器">
<li>
<section><strong>watch</strong>：chokidar 监听 <code>MEMORY.md</code>、<code>memory.md</code>、<code>memory/**/*.md</code>（以及 extraPaths、多模态扩展），变更标记 dirty，debounce 后 <code>sync(reason="watch")</code>。</section>
</li>
<li>
<section><strong>interval</strong>：<code>sync.intervalMinutes&gt;0</code> 就 <code>setInterval</code> 定时跑。</section>
</li>
<li>
<section><strong>onSessionStart</strong>：search 前 <code>warmSession(sessionKey)</code>，若开了 <code>sync.onSessionStart</code>，每个 sessionKey 首次触发后台 sync。</section>
</li>
<li>
<section><strong>onSearch</strong>：search 时如果 dirty 且开了 <code>sync.onSearch</code>，后台触发 sync。</section>
</li>
<li>
<section><strong>session-delta</strong>：监听 transcript 更新，累计新增 bytes/lines，达到阈值后把相关 session 文件标脏，再 sync。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">还有两点：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><strong>单飞锁</strong>：同一时刻只跑一个 sync，复用同一个 Promise（参考内容定位 <code>manager.ts:452-467</code>）。</section>
</li>
<li>
<section><strong>全量重建的原子 swap</strong>：写到 <code>.tmp-UUID</code>，完成后 swap（含 <code>wal/-shm</code>），避免半成品索引（参考内容定位 <code>manager-sync-ops.ts:1050-1158</code>）。</section>
</li>
</ul>
<h1 data-tool="mdnice编辑器"><span class="content">6. 召回是怎么发生的</span></h1>
<p data-tool="mdnice编辑器">主要看 <code>buildMemorySection()</code> 的代码，因为它把策略写死了：</p>
<pre class="custom" data-tool="mdnice编辑器"><code class="hljs">提示词：
<span class="hljs-keyword">function</span> buildSkillsSection(params: { skillsPrompt?: string; readToolName: string }) {
  const trimmed = params.skillsPrompt?.trim();
  <span class="hljs-keyword">if</span> (!trimmed) {
    <span class="hljs-built_in">return</span> [];
  }
  <span class="hljs-built_in">return</span> [
    <span class="hljs-string">"## Skills (mandatory)"</span>,
    <span class="hljs-string">"Before replying: scan &lt;available_skills&gt; &lt;description&gt; entries."</span>,
    `- If exactly one skill clearly applies: <span class="hljs-built_in">read</span> its SKILL.md at &lt;location&gt; with \`<span class="hljs-variable">${params.readToolName}</span>\`, <span class="hljs-keyword">then</span> follow it.`,
    <span class="hljs-string">"- If multiple could apply: choose the most specific one, then read/follow it."</span>,
    <span class="hljs-string">"- If none clearly apply: do not read any SKILL.md."</span>,
    <span class="hljs-string">"Constraints: never read more than one skill up front; only read after selecting."</span>,
    <span class="hljs-string">"- When a skill drives external API writes, assume rate limits: prefer fewer larger writes, avoid tight one-item loops, serialize bursts when possible, and respect 429/Retry-After."</span>,
    trimmed,
    <span class="hljs-string">""</span>,
  ];
}

<span class="hljs-keyword">function</span> buildMemorySection(params: {
  isMinimal: boolean;
  availableTools: Set&lt;string&gt;;
  citationsMode?: MemoryCitationsMode;
}) {
  <span class="hljs-keyword">if</span> (params.isMinimal) {
    <span class="hljs-built_in">return</span> [];
  }
  <span class="hljs-keyword">if</span> (!params.availableTools.has(<span class="hljs-string">"memory_search"</span>) &amp;&amp; !params.availableTools.has(<span class="hljs-string">"memory_get"</span>)) {
    <span class="hljs-built_in">return</span> [];
  }
  const lines = [
    <span class="hljs-string">"## Memory Recall"</span>,
    <span class="hljs-string">"Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines. If low confidence after search, say you checked."</span>,
  ];
  <span class="hljs-keyword">if</span> (params.citationsMode === <span class="hljs-string">"off"</span>) {
    lines.push(
      <span class="hljs-string">"Citations are disabled: do not mention file paths or line numbers in replies unless the user explicitly asks."</span>,
    );
  } <span class="hljs-keyword">else</span> {
    lines.push(
      <span class="hljs-string">"Citations: include Source: &lt;path#line&gt; when it helps the user verify memory snippets."</span>,
    );
  }
  lines.push(<span class="hljs-string">""</span>);
  <span class="hljs-built_in">return</span> lines;
}

工具描述：
 label: <span class="hljs-string">"Memory Search"</span>,
    name: <span class="hljs-string">"memory_search"</span>,
    description:
      <span class="hljs-string">"Mandatory recall step: semantically search MEMORY.md + memory/*.md (and optional session transcripts) before answering questions about prior work, decisions, dates, people, preferences, or todos; returns top snippets with path + lines. If response has disabled=true, memory retrieval is unavailable and should be surfaced to the user."</span>,
    parameters: MemorySearchSchema,
</code></pre>
<p data-tool="mdnice编辑器">有三点：</p>
<ol data-tool="mdnice编辑器">
<li>
<section><strong>触发条件写得具体</strong>：prior work / decisions / dates / people / preferences / todos。模型不需要猜「算不算记忆相关」。</section>
</li>
<li>
<section><strong>两阶段召回</strong>：先 <code>memory_search</code> 找片段，再 <code>memory_get</code> 精读少量行，控制注入体积。</section>
</li>
<li>
<section><strong>引用策略可控</strong>：<code>citationsMode</code> 可以关掉，避免模型动不动把路径行号甩出来（对产品形态很重要）。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">两阶段召回比「一次性把相关文件 wholefile 塞进去」靠谱太多。</p>
<h1 data-tool="mdnice编辑器"><span class="content">7. 召回结果怎么进上下文</span></h1>
<p data-tool="mdnice编辑器">toolResult 消息是关键通道</p>
<p data-tool="mdnice编辑器">很多人以为「记忆」是把内容写进 system prompt。OpenClaw 不是这么干的。</p>
<p data-tool="mdnice编辑器">召回结果会以工具执行结果的形式进入会话消息列表，后续模型调用自然「看得到」。</p>
<ul data-tool="mdnice编辑器">
<li>
<section>工具返回会被包装成 JSON 文本块（参考内容定位 <code>jsonResult()</code> 在 <code>src/agents/tools/common.ts:230-239</code>）。</section>
</li>
<li>
<section>tool 返回会被标准化成 <code>content[] + details</code>（参考内容定位 <code>src/agents/pi-tool-definition-adapter.ts</code> 的 normalize）。</section>
</li>
<li>
<section>这些 toolResult 会被追加到 session messages，下一次模型调用会携带。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">这条通道对排障非常友好：我们可以在 transcript 里看到「这次回答之前它到底召回了什么」。而且 toolResult 天然可控预算、可控格式，比让模型把记忆揉进自由文本稳得多。</p>
<h1 data-tool="mdnice编辑器"><span class="content">8. 写入时机</span></h1>
<h2 data-tool="mdnice编辑器"><span class="content">8.1 会话快照写入</span></h2>
<p data-tool="mdnice编辑器">人为触发的「切会话」</p>
<p data-tool="mdnice编辑器">当执行 <code>/new</code>、<code>/reset</code> 时，上一段会话尾部会被抽取成 <code>YYYY-MM-DD-&lt;slug&gt;.md</code>。这是「防断片」写入，价值是保住最近上下文。</p>
<p data-tool="mdnice编辑器">坑：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>抽取的 N 条对话里可能包含敏感信息。它会落在 <code>memory/</code>，并进入索引。</section>
</li>
<li>
<section>如果你把 workspace 目录同步到团队共享盘或提交到 repo，泄露面会扩大。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">我们的做法：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>明确区分「个人 workspace」和「团队 workspace」。个人的 <code>memory/</code> 默认不进 repo。</section>
</li>
<li>
<section>开启 citations 时，产品侧要想清楚是否允许暴露路径与行号。</section>
</li>
</ul>
<h2 data-tool="mdnice编辑器"><span class="content">8.2 短期记忆写入</span></h2>
<p data-tool="mdnice编辑器">「需要我们调教，告诉她哪些重要」。</p>
<p data-tool="mdnice编辑器">短期记忆要走「稀疏高密度」路线：条目少，但每条都能在未来的某个问题上直接复用。写入策略要围绕「将来会搜什么」来定，不要围绕「当下发生了什么」来记流水账。</p>
<h2 data-tool="mdnice编辑器"><span class="content">8.3 长期记忆更新</span></h2>
<p data-tool="mdnice编辑器">心跳 / AGENTS.md / cron</p>
<p data-tool="mdnice编辑器">三种更新机制：心跳、核心流程、cron。</p>
<p data-tool="mdnice编辑器">读最近几天短期记忆 → 选值得长期记住的 → 提炼写入 <code>MEMORY.md</code>。</p>
<p data-tool="mdnice编辑器">观点：</p>
<ul data-tool="mdnice编辑器">
<li>
<section><strong>cron 最稳</strong>，可观测、可控、可回滚。</section>
</li>
<li>
<section>心跳更新很容易在负载高时抖动，或者在你最不想更新的时候更新。</section>
</li>
<li>
<section>把它塞进核心流程（AGENTS.md）要谨慎，一旦每次任务都触发提炼，会把延迟拉上去。</section>
</li>
</ul>
<p data-tool="mdnice编辑器">一般做法：</p>
<ul data-tool="mdnice编辑器">
<li>
<section>工作日每天固定一次 consolidation（cron）。</section>
</li>
<li>
<section>遇到重大决策或事故复盘，当天手动提炼写入 <code>MEMORY.md</code>，不等自动化。</section>
</li>
</ul>
<h1 data-tool="mdnice编辑器"><span class="content">9. 怎么「调教」</span></h1>
<p data-tool="mdnice编辑器">把记忆当成协议，不当成日记</p>
<p data-tool="mdnice编辑器">「告诉她哪些重要」。把「重要」拆成几类，每类有明确写入规则，避免模型自由发挥。</p>
<p data-tool="mdnice编辑器">一般的规则：</p>
<ol data-tool="mdnice编辑器">
<li>
<section><strong>稳定偏好</strong>：例如输出格式偏好、技术栈偏好、代码风格偏好。</section>
</li>
<li>
<section><strong>组织事实</strong>：团队结构、系统边界、核心服务依赖、环境约束。</section>
</li>
<li>
<section><strong>关键决策</strong>：ADR 级别的决策，包含时间点与理由。</section>
</li>
<li>
<section><strong>长期目标与在途事项</strong>：能跨周追踪的，不写「今天要做的」。</section>
</li>
<li>
<section><strong>事故教训</strong>：明确到「哪个坑踩过」「如何避免」。</section>
</li>
</ol>
<p data-tool="mdnice编辑器">短期记忆（<code>memory/YYYY-MM-DD.md</code>）会允许更多过程性信息，但要满足一个条件：<strong>未来能被搜索问题命中</strong>。比如你写「今天讨论了 A」，基本没用；你写「决定 A 的原因是 B，后续若出现 C 用 D 回滚」，会有用一些。</p>
<p data-tool="mdnice编辑器">如果希望 <code>memory_search</code> 在关键时刻召回到正确内容，就得用「未来的查询语句」来写记忆。</p>
<p data-tool="mdnice编辑器">以上。</p>
</section>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2026/03/openclaw-memory/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>如何构建行业 Agent 的 RAG</title>
		<link>https://www.phppan.com/2025/12/how-to-build-an-industry-agent-rag/</link>
		<comments>https://www.phppan.com/2025/12/how-to-build-an-industry-agent-rag/#comments</comments>
		<pubDate>Sun, 21 Dec 2025 00:28:37 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[Agent]]></category>
		<category><![CDATA[RAG]]></category>

		<guid isPermaLink="false">https://www.phppan.com/?p=2447</guid>
		<description><![CDATA[行业 Agent 和我们常用的「通用聊天 Agent」不是一类东西。 行业 Agent 是要能解决问题的，而查 [&#8230;]]]></description>
				<content:encoded><![CDATA[<p style="color: #191b1f;" data-first-child="" data-pid="_Fvj3qAo">行业 Agent 和我们常用的「通用聊天 Agent」不是一类东西。</p>
<p style="color: #191b1f;" data-pid="WXgeV_Jl">行业 Agent 是要能解决问题的，而查资料只是其中一步，后面还要做判断、走流程、调用系统、校验结果、留痕、可回放。</p>
<p style="color: #191b1f;" data-pid="sRRsjjmo">RAG 在这里的角色也变了：它不只是给模型喂上下文，而是给 Agent 提供可执行任务所需的依据、约束、参数和证据链。</p>
<p style="color: #191b1f;" data-pid="7G-ev8ms">今天我们聊一下行业 Agent 构建过程中的 RAG 怎么写，从目标、数据，检索以及使用 RAG 等等方面。</p>
<h2 style="font-weight: 500; color: #191b1f;">1. 行业 Agent 的 RAG 要服务什么能力</h2>
<p style="color: #191b1f;" data-pid="uXdOzN2n">行业 Agent 常见的工作方式是「多步闭环」：</p>
<ol style="color: #191b1f;">
<li data-pid="p-6h7mCf">识别问题类型与业务对象（客户、设备、合同、工单、订单、项目、账号等）</li>
<li data-pid="Au8xLHqd">查依据（制度、手册、知识库、历史工单、标准、接口文档）</li>
<li data-pid="N-o99Kui">做动作（查系统、下发指令、开通配置、生成工单、发邮件、写报告、提审批）</li>
<li data-pid="F4mSbwYG">校验与回写（确认变更成功、回填字段、留痕、把引用/证据挂到工单）</li>
<li data-pid="Doi6TI-9">解释（给用户说明依据、影响范围、回滚方案、下一步建议）</li>
</ol>
<p style="color: #191b1f;" data-pid="_N4MGLsp">所以行业 Agent 的 RAG 不只是「问答检索」，而是至少要覆盖这些信息类型：</p>
<ul style="color: #191b1f;">
<li data-pid="vUZWWyko">规则依据：制度、条款、SOP、合规模板、变更规范</li>
<li data-pid="4Q_phUiD">操作依据：系统使用手册、接口文档、参数含义、错误码处理</li>
<li data-pid="tXaTETlZ">对象事实：来自业务系统的实时/准实时数据（用户信息、资源状态、库存、账单、设备状态）</li>
<li data-pid="V1-Otm3r">历史经验：工单处理记录、故障复盘、已知问题（KEDB）</li>
<li data-pid="uHSEdNWt">风险边界：禁用操作清单、权限范围、需要人工复核的条件</li>
</ul>
<p style="color: #191b1f;" data-pid="zea6g6Y_">如果我们只做「文档向量库 + 生成」，Agent 走到第 3 步就会卡：它不知道该调用哪个系统、需要哪些字段、什么情况下要停下来让人确认，也不知道怎么证明自己做对了。</p>
<h2 style="font-weight: 500; color: #191b1f;">2. 指标</h2>
<p style="color: #191b1f;" data-pid="O82pWPQe">行业 Agent 场景里，最好用三类指标描述：</p>
<h2 style="font-weight: 500; color: #191b1f;">2.1 任务完成类指标</h2>
<ul style="color: #191b1f;">
<li data-pid="vDw37fwW">任务成功率（最终动作成功并通过校验）</li>
<li data-pid="BIM5gkqU">平均完成时长（端到端）</li>
<li data-pid="DoybAuEk">人工介入率（需要人确认/补充信息/兜底）</li>
<li data-pid="uWRt7jzd">回滚率（执行后需要撤销/修正）</li>
</ul>
<h2 style="font-weight: 500; color: #191b1f;">2.2 风险类指标（红线不能过）</h2>
<ul style="color: #191b1f;">
<li data-pid="UkS0Yulz">越权率（检索/执行是否越权，目标是 0）</li>
<li data-pid="23WkdZU3">误执行率（不该执行却执行）</li>
<li data-pid="HaUEWinC">误答导致的错误操作（把“编出来的依据”当成执行依据）</li>
<li data-pid="PWRr1Ece">引用不可追溯率（给不出来源或来源不支持结论）</li>
</ul>
<h2 style="font-weight: 500; color: #191b1f;">2.3 知识与检索类指标（用于驱动迭代）</h2>
<ul style="color: #191b1f;">
<li data-pid="UimFGQqD">依据命中率（标准依据是否出现在 topK）</li>
<li data-pid="4IFYRJoU">冲突处理正确率（新旧版本/多来源冲突时是否选对）</li>
<li data-pid="VC8kim8H">时效正确率（是否引用过期/废止内容）</li>
<li data-pid="GHbz90Yw">覆盖率（高频问题是否覆盖）</li>
</ul>
<p style="color: #191b1f;" data-pid="0sKYnSs8">行业 Agent 的 RAG 设计，最终要对这些指标负责。否则我们会陷入「答得像那么回事，但不敢让它动系统」的状态。</p>
<h2 style="font-weight: 500; color: #191b1f;">3. 数据层</h2>
<p style="color: #191b1f;" data-pid="5DoZhpBN">行业 Agent 的 RAG，数据比模型更重要。</p>
<h2 style="font-weight: 500; color: #191b1f;">3.1 三类数据</h2>
<ol style="color: #191b1f;">
<li data-pid="tHE3e0ob">静态权威知识：制度、规范、手册、标准、产品文档<br />
目标：可追溯、版本可控、可引用</li>
<li data-pid="v5mHpX-R">动态业务事实：来自业务系统的数据（CRM、工单、CMDB、监控、计费、IAM 等）<br />
目标：可校验、可审计、最好可回放（至少保留查询快照）</li>
<li data-pid="uH14OYOe">过程与经验：历史工单、故障复盘、处理记录、FAQ 演进<br />
目标：可过滤（质量参差）、可分级（权威/经验/猜测）</li>
</ol>
<p style="color: #191b1f;" data-pid="ooh0QN8F">很多项目失败是因为把第 3 类当第 1 类用，把「经验」当「制度」。Agent 一旦据此去执行动作，风险会放大。</p>
<h2 style="font-weight: 500; color: #191b1f;">3.2 每个知识片段必须带的元数据</h2>
<p style="color: #191b1f;" data-pid="BS_nzuoS">行业 Agent 需要的不只是「能搜到」，还要「能用来做动作」。建议每个 chunk 至少包含：</p>
<ul style="color: #191b1f;">
<li data-pid="UrcHCaaQ"><code>doc_id / chunk_id</code></li>
<li data-pid="IMDzWu8U"><code>source</code>（系统/库）</li>
<li data-pid="5I0KQ3ng"><code>source_url</code>（可点击或可定位）</li>
<li data-pid="1T95YkZb"><code>title_path</code>（标题链）</li>
<li data-pid="ob9rQ8Ro"><code>doc_type</code>（制度/手册/接口文档/复盘/工单等）</li>
<li data-pid="HrFHrpVF"><code>version</code>、<code>status</code>（草稿/已发布/已废止）</li>
<li data-pid="zVhGgMLu"><code>effective_from / effective_to</code>（能给就给）</li>
<li data-pid="wQ1KNNdl"><code>owner</code>（维护人/团队）</li>
<li data-pid="-MObHdR0"><code>updated_at</code></li>
<li data-pid="WsMjhehk">适用范围标签：产品线/地区/客户/机型/环境（生产/测试）</li>
<li data-pid="-8_Qkt1y">权限标签：RBAC/ABAC 所需字段</li>
<li data-pid="FyU8NJiA">可执行性标签（建议加）：
<ul>
<li data-pid="U1X2WHsV">是否可作为执行依据（例如制度/已发布 SOP 才能）</li>
<li data-pid="c4cWmBhG">是否需要人工复核（高风险操作）</li>
<li data-pid="YqzlLX0m">是否仅供参考（复盘/经验）</li>
</ul>
</li>
</ul>
<p style="color: #191b1f;" data-pid="EcfVjtBv">这些标签对 Agent 比较关键：它能决定「能不能做、要不要停、怎么解释」。</p>
<h2 style="font-weight: 500; color: #191b1f;">3.3 文档解析与切分</h2>
<p style="color: #191b1f;" data-pid="erJ-fH7-">行业 Agent 的 RAG 的切分策略，优先级一般是：</p>
<ol style="color: #191b1f;">
<li data-pid="Nxb14Xvu">按结构切：章/节/条款/接口字段说明/错误码条目</li>
<li data-pid="9r74YqhK">把“前置条件/限制/例外”跟规则放一起</li>
<li data-pid="yCw_OU03">表格与字段定义要保表头（字段含义脱离表头就没法用）</li>
<li data-pid="JUNyz8m4">把可执行步骤单独成块（SOP、Runbook、变更步骤）</li>
</ol>
<p style="color: #191b1f;" data-pid="h_cZ_tiE">注意：不要把「定义」「适用范围」「例外条款」切碎。Agent 执行动作时，最需要的就是边界条件和限制。</p>
<h2 style="font-weight: 500; color: #191b1f;">4. 索引与检索</h2>
<p style="color: #191b1f;" data-pid="NSbRT0Bg">行业 Agent 和常规的 Agent 不同，其更依赖于「过滤 + 排序 + 证据链」</p>
<h2 style="font-weight: 500; color: #191b1f;">4.1 使用混合检索</h2>
<p style="color: #191b1f;" data-pid="_KRV8TKw">行业 Agent 的查询里会出现大量「硬信息」：</p>
<ul style="color: #191b1f;">
<li data-pid="vACEVG4-">条款号、标准号、型号、错误码、参数名、接口路径、工单号、配置项名</li>
</ul>
<p style="color: #191b1f;" data-pid="p4STkNYM">纯向量在这些场景不稳。工程上更常用的是：</p>
<ul style="color: #191b1f;">
<li data-pid="AkBe0Uxa">关键词/BM25：抓编号、术语、字段名、错误码</li>
<li data-pid="tLpgvb1T">向量召回：抓语义相近、同义表达</li>
<li data-pid="MZFmCh3q">融合 + 重排：把候选集排序成「最能支持动作/结论」的那几段</li>
</ul>
<h2 style="font-weight: 500; color: #191b1f;">4.2 检索要先过滤，再找相似</h2>
<p style="color: #191b1f;" data-pid="s5g_6N7O">行业 Agent 的过滤通常是强约束，如下：</p>
<ul style="color: #191b1f;">
<li data-pid="hVUIt9Yj">权限过滤（用户/角色/租户/数据域）</li>
<li data-pid="EJN2N9Ra">状态过滤（废止、草稿默认不进）</li>
<li data-pid="5KW723mG">生效时间过滤（尤其制度、计费、合规）</li>
<li data-pid="2z1o3WUL">适用范围过滤（产品/地区/环境）</li>
<li data-pid="bZfdhNZn">数据域隔离（内部/客户侧/合作方）</li>
</ul>
<p style="color: #191b1f;" data-pid="Pf6CepOO">如果我们把这些留到生成阶段「让模型自己注意」，效果不可控，风险也不可控。</p>
<h2 style="font-weight: 500; color: #191b1f;">4.3 Agent 专用检索</h2>
<p style="color: #191b1f;" data-pid="fAn6MGCV">不止检索答案，还要检索工具与参数</p>
<p style="color: #191b1f;" data-pid="ZUDTVbUT">行业 Agent 经常需要两类额外检索：</p>
<ol style="color: #191b1f;">
<li data-pid="14T14GaS">工具检索（Tool RAG）<br />
从「工具说明库/接口文档/SOP」里检索：该用哪个工具、需要哪些参数、有哪些限制、失败怎么处理。</li>
<li data-pid="wGAjt21i">参数与字段检索（Schema RAG）<br />
从「数据字典/字段说明/枚举值」里检索：字段含义、可选值、校验规则、示例格式。</li>
</ol>
<p style="color: #191b1f;" data-pid="KHbJc-N5">这两类检索的结果不一定直接展示给用户，但会决定 Agent 能不能把动作做对。</p>
<h2 style="font-weight: 500; color: #191b1f;">5. 固化 Agent 使用 RAG 的逻辑</h2>
<p style="color: #191b1f;" data-pid="NwwBvADg">行业 Agent 的 RAG 关键是要「把 RAG 插进决策点」</p>
<p style="color: #191b1f;" data-pid="utLREmJs">行业 Agent 常见的内部循环大致是：</p>
<ul style="color: #191b1f;">
<li data-pid="5QP9RazX">Plan（决定下一步做什么）</li>
<li data-pid="HuVTSCsA">Act（调用工具/检索/执行）</li>
<li data-pid="jCCN_5WI">Observe（拿到结果）</li>
<li data-pid="bFfS8rOI">Decide（是否继续、是否需要人确认、是否结束）</li>
<li data-pid="yQis0Ph_">Explain（对外输出）</li>
</ul>
<p style="color: #191b1f;" data-pid="geSOjf6D">RAG 的插入点建议固定成三处：</p>
<h2 style="font-weight: 500; color: #191b1f;">5.1 决策前</h2>
<p style="color: #191b1f;" data-pid="j4rsmM_L">用 RAG 找「规则边界」</p>
<p style="color: #191b1f;" data-pid="TrpGaLJ2">在 Agent 做出关键决策前，先检索：</p>
<ul style="color: #191b1f;">
<li data-pid="jKX832Od">是否允许执行（权限、合规、风险等级）</li>
<li data-pid="tvEGzrOh">前置条件是什么（必须具备哪些信息、哪些系统状态）</li>
<li data-pid="lO28-IyB">需要的审批/确认是什么（是否必须人工确认）</li>
</ul>
<p style="color: #191b1f;" data-pid="YKn704LY">这一步的输出的是「约束」，不是「答案」。它会影响下一步是继续、暂停还是转人工。</p>
<h2 style="font-weight: 500; color: #191b1f;">5.2 执行前</h2>
<p style="color: #191b1f;" data-pid="JEd3Df9z">用 RAG 找「操作步骤与参数」</p>
<p style="color: #191b1f;" data-pid="bV2olvUo">执行某个动作前，检索：</p>
<ul style="color: #191b1f;">
<li data-pid="l4Qrf07r">SOP / Runbook / 接口文档</li>
<li data-pid="lSUYod3J">必填参数、参数来源</li>
<li data-pid="R967qsEd">校验方式（执行后如何确认成功）</li>
<li data-pid="iODcwcjM">回滚方式（失败/异常如何撤销）</li>
</ul>
<p style="color: #191b1f;" data-pid="A4ixm2P9">这一步的输出是「可执行步骤」，不是「解释性段落」。</p>
<h2 style="font-weight: 500; color: #191b1f;">5.3 执行后</h2>
<p style="color: #191b1f;" data-pid="K1dShMjs">用 RAG 做「结果判定与错误处理」</p>
<p style="color: #191b1f;" data-pid="3hCIsVEm">拿到工具返回值后，检索：</p>
<ul style="color: #191b1f;">
<li data-pid="TGhuHlpO">错误码含义与处理建议</li>
<li data-pid="VwLnmXsp">常见失败原因</li>
<li data-pid="wIZQfkwm">是否需要升级/转人工</li>
<li data-pid="5q63MWOx">是否需要二次校验（比如跨系统一致性）</li>
</ul>
<p style="color: #191b1f;" data-pid="QQ-5_La6">这一步的输出是「下一步动作建议 + 证据」。</p>
<h2 style="font-weight: 500; color: #191b1f;">6. 生成与输出</h2>
<p style="color: #191b1f;" data-pid="C2vOXebE">行业 Agent 的输出要分层，不要把所有东西都写给用户</p>
<p style="color: #191b1f;" data-pid="Q1aGGdiT">行业 Agent 的输出建议拆成三层，分别服务不同目标：</p>
<ol style="color: #191b1f;">
<li data-pid="e2okJ3Ur">用户层：结论/进展、需要用户补充什么、下一步怎么走</li>
<li data-pid="S-Dc5Ozd">证据层：引用依据（链接、页码、版本、生效日期）</li>
<li data-pid="ePI1OPwy">执行层（留痕层）：本次调用了什么工具、参数摘要、返回结果摘要、校验结果、回滚点</li>
</ol>
<p style="color: #191b1f;" data-pid="GOh0WCCx">用户不一定要看到执行层细节，但系统必须存储这些内容。只有出了问题能回放，才敢放权。</p>
<p style="color: #191b1f;" data-pid="1N9Qf3GW">同时，行业 Agent 的生成要有硬规则：</p>
<ul style="color: #191b1f;">
<li data-pid="k6x4YRop">没有命中权威依据：不输出肯定结论</li>
<li data-pid="CpZgMUEE">有冲突：必须把冲突来源、版本、生效时间写清楚</li>
<li data-pid="B0ulMWqI">涉及高风险动作：必须停下来请求确认（并把依据与影响范围给出来）</li>
<li data-pid="1t_nnq5B">引用必须来自检索上下文：不允许来虚的，「凭印象补一句」</li>
</ul>
<h2 style="font-weight: 500; color: #191b1f;">7. 权限、审计、隔离</h2>
<p style="color: #191b1f;" data-pid="_qucgNvg">**行业 Agent 的 RAG 必须「检索前隔离」。</p>
<p style="color: #191b1f;" data-pid="IknS6jPO">行业 Agent 一旦能调用系统，风险比问答高一个量级。权限要分两层：</p>
<h2 style="font-weight: 500; color: #191b1f;">7.1 知识权限</h2>
<p style="color: #191b1f;" data-pid="fddZRlzb">能不能看的问题</p>
<ul style="color: #191b1f;">
<li data-pid="-pqsTCpx">文档/知识片段按 ABAC/RBAC 做过滤</li>
<li data-pid="zKYBTslW">按租户隔离（多客户必做）</li>
<li data-pid="ccEPz6QX">按数据域隔离（内部策略、客户信息、合作方信息）</li>
</ul>
<h2 style="font-weight: 500; color: #191b1f;">7.2 行为权限</h2>
<p style="color: #191b1f;" data-pid="DrtoS4ke">能不能做的问题</p>
<ul style="color: #191b1f;">
<li data-pid="yf_bGSvS">工具级权限：这个角色能调用哪些工具</li>
<li data-pid="Mg0F7lJP">动作级权限：同一工具下哪些操作允许（例如只读查询 vs 修改/下发）</li>
<li data-pid="e23NNoWJ">参数级权限：同一动作下哪些资源范围允许（例如仅能操作自己负责的项目/客户）</li>
</ul>
<p style="color: #191b1f;" data-pid="W9LkAw1r">很多团队只做了「知识权限」，没做「行为权限」。</p>
<p style="color: #191b1f;" data-pid="LYVNsc-P">这会导致不放心，即使 Agent 能查到 SOP，也能学会「怎么做」，但你又不敢让它真的做。</p>
<h2 style="font-weight: 500; color: #191b1f;">7.3 审计要能回答四个问题</h2>
<ul style="color: #191b1f;">
<li data-pid="2w1BwWh8">为什么这么做（依据是什么）</li>
<li data-pid="lzGpWZeB">做了什么（调用了哪些工具、关键参数是什么）</li>
<li data-pid="2JYfo8uy">得到了什么（返回结果与校验结果）</li>
<li data-pid="V3fDIMvK">谁批准的（如果需要人工确认）</li>
</ul>
<p style="color: #191b1f;" data-pid="MET_gAHK">没有这四个问题的答案，行业 Agent 很难通过安全审查，也很难在出事后定位责任与修复点。</p>
<h2 style="font-weight: 500; color: #191b1f;">8. 灰度上线策略</h2>
<p style="color: #191b1f;" data-pid="vY9VAGHp">先控制风险，再谈覆盖率</p>
<p style="color: #191b1f;" data-pid="wKd4eAcH">行业 Agent 的上线节奏建议按权限逐步放开：</p>
<ol style="color: #191b1f;">
<li data-pid="A2LwivTU">只读 Agent：只检索、只解释、只给建议，不执行任何写操作</li>
<li data-pid="pq15yn4h">半自动 Agent：可以生成“执行计划/工单草稿/变更单草稿”，必须人工确认后执行</li>
<li data-pid="RdecVrZG">受限自动 Agent：只允许低风险、可回滚、可校验的动作自动执行（例如查询、对账、生成报表、创建工单、补全字段）</li>
<li data-pid="Odv0fyay">高风险动作：默认保留人工确认，除非你能做到严格的权限、校验、回滚、审计，并且有明确的责任边界</li>
</ol>
<p style="color: #191b1f;" data-pid="P5l39kjs">上线必须准备三套兜底：</p>
<ul style="color: #191b1f;">
<li data-pid="zU2V0L-7">超时与降级：检索失败/重排失败/模型失败时怎么退化</li>
<li data-pid="DmCAPAhB">失败回滚：执行失败怎么撤销，撤销失败怎么升级</li>
<li data-pid="xG48Jluj">人工接管：在关键节点能一键转人工，并把证据与执行轨迹带过去</li>
</ul>
<h2 style="font-weight: 500; color: #191b1f;">9. 常见坑</h2>
<ol style="color: #191b1f;">
<li data-pid="X26krG5-">把「经验工单」当「标准答案」：Agent 会把偶发处理当成通用规则。必须分级与降权。</li>
<li data-pid="jTOjzyDW">只做知识库，不做数据字典与工具库：Agent 会不知道参数怎么填、字段是什么意思、错误码怎么解。</li>
<li data-pid="dUkG-A76">只做检索，不做执行前校验与执行后校验：敢执行的前提是可校验、可回滚。</li>
<li data-pid="cexlm6HA">权限只管文档，不管工具：最容易在这里翻车。</li>
<li data-pid="E3WwsGTa">没有回放评测：你不知道一次小改动会不会让 Agent 在某个分支上开始乱走。</li>
<li data-pid="gzD5ZMai">把「多轮对话」当「任务编排」：行业 Agent 的关键是状态机与决策点，不是聊得多。</li>
</ol>
<p style="color: #191b1f;" data-pid="m6vPrXqs">最后，行业 Agent 的 RAG 如果要构建，不仅仅是算法的事情，需要更多的业务专家，业务 Owner 来直接参与，他们才是最懂行业的人。需要他们来定义「什么算答对/答错」、哪些文档权威、版本如何取舍、哪些内容不能答。</p>
<p style="color: #191b1f;" data-pid="iKsLaI1L">以上。</p>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2025/12/how-to-build-an-industry-agent-rag/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
