<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/feeds/rss-style.xsl" type="text/xsl"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>WakeUp-Jin</title>
        <link>https://wakeup-jin-blog.netlify.app/</link>
        <description>WakeUp-Jin 的个人博客，记录上下文工程、Agent Harness 与大模型应用开发的实践与思考。</description>
        <lastBuildDate>Fri, 31 Jul 2026 04:42:46 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Astro-Theme-Retypeset with Feed for Node.js</generator>
        <language>zh</language>
        <copyright>Copyright © 2026 WakeUp-Jin</copyright>
        <atom:link href="https://wakeup-jin-blog.netlify.app/rss.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Anthropic 黑客马拉松冠军：ClaudeCode 配置整理和补充]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/anthropic-hackathon-claudecode/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/anthropic-hackathon-claudecode/</guid>
            <pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[整理黑客马拉松冠军 Claude Code 配置合集中的核心模式：跨会话共享内存、持续学习、检查点评估、代码地图与子智能体编排。]]></description>
            <content:encoded><![CDATA[<h2>前言</h2>
<p>分析参考来源：</p>
<ul>
<li>黑客马拉松冠军 Claude Code 配置合集：https://github.com/affaan-m/everything-claude-code</li>
<li>Anthropic 官方 Skill 配置说明：https://github.com/anthropics/skills</li>
</ul>
<p>::github{repo="affaan-m/everything-claude-code"}</p>
<ul>
<li>ClaudeCode 配置文件：https://code.claude.com/docs/en/sub-agents</li>
</ul>
<h2>一、跨会话共享内存</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-64.hNeDppAV_Z1W7L4h.webp" alt="跨会话共享内存" /></p>
<p>在使用 ClaudeCode 的过程中，会话记录虽然可以被保存到本地文件中，也可以使用 resume 指令继续上一次对话，但是完整会话记录会被压缩，如果经过多次压缩，关键的决策信息会被不断的稀释，直到完全忘记，那么我理解的关键信息就是如下四点：</p>
<ol>
<li><strong>哪些方法是有效的（有证据可以验证）</strong></li>
<li><strong>哪些尝试过的方法是无效的</strong></li>
<li><strong>哪些方法尚未尝试</strong></li>
<li><strong>哪些未完成的工作</strong></li>
</ol>
<p>🪐所以为了保证上面四点关键的信息能够在多个会话共享，就需要有一个<strong>单独的中间态的临时会话文件</strong></p>
<p>这个时候是需要完整的<strong>文件创建和保存的自动化流程，还有填充内容的指令</strong>，可以采用三种 hook 来解决这个文件的创建、加载、保存的问题，</p>
<ol>
<li>预压缩钩子（<strong>PreCompact Hook）</strong>：在上下文压缩发生之前，将重要状态保存至文件</li>
<li>会话完成钩子（<strong>SessionComplete Hook）</strong>：会话结束时，将学习成果持久化至文件或初始化文件</li>
<li>会话开始钩子（<strong>SessionStart Hook）</strong>：新会话启动时，自动加载先前上下文，并输出最新文件的路径</li>
</ol>
<p>那么这个临时文件（会话记录文件）我们要填充什么内容进去？，怎么填充？</p>
<ul>
<li>填充的内容：可以根据上面四点信息方向来让 Claude 总结会话历史、你也可以手动编写该文件，整理写入你认为重要的会话关键信息</li>
<li>填充的方式：你可以在聊天会话中主动提及，也可以创建相应的 Skill 和 Command 来使用</li>
</ul>
<p>🌴 这个模式下使用到的文件 hook 为：</p>
<ul>
<li>scripts/hooks/session-start.js：会话开始钩子</li>
<li>scripts/hooks/pre-compact.js：会话压缩钩子</li>
<li>scripts/hooks/session-end.js：会话完成钩子</li>
</ul>
<h2>二、持续学习并更新记忆</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-65.CE5d9aQ0_Z1l5puz.webp" alt="持续学习流程" /></p>
<p>关于持续学习并更新记忆的触发方式有两种：<strong>一种是 Hook 的挂载脚本自动执行的方式、另外一种是 Command 命令手动执行的方式</strong></p>
<p>🧩关于 Hook 的挂载脚本自动执行的方式，该方式使用了三种不同时机的 Hook 来触发</p>
<ol>
<li><strong>Stop</strong> 的 Hook 的具体逻辑：
<ol>
<li>简单的对于会话列表的长度进行判断</li>
<li>如果长度达标、那么输出提示词信息给用户看</li>
</ol>
</li>
<li><strong>Sessionend</strong> 的 Hook 的具体逻辑：
<ol>
<li>读取完整的会话历史记录</li>
<li>通过 claude -p "xxx"，来调用 claude 生成学习记录</li>
</ol>
</li>
<li><strong>PostToolUse</strong> 的 Hook 的具体逻辑：
<ol>
<li>简单的进行会话列表的长度判断</li>
<li>将"总结学习记录的指令"放入到工具返回结果，以此输入给 Claude 触发判断</li>
</ol>
</li>
</ol>
<p>🧩关于 Cmmand 命令手动执行的方式、该方式设计了/learn 指令来执行</p>
<p>当用户在会话中完成任务的时候，发现有一些设计方案非常值得保存到记忆中，这个时候就可以触发/learn 指令</p>
<p><strong>所有总结下来的学习记录都存放在/skill/learn 文件夹中，这样或许 Agent 可以自动根据具体情况使用 Skill 的学习记录的技能</strong></p>
<p>🌴 模式下使用到的文件 Command 和 Skill 为：</p>
<ul>
<li>commands/learn.md</li>
<li>skills/continuous-learning</li>
</ul>
<p>👉 小拓展：
关于会话记录文件 (Session Tmp) 和学习记录文件 (Learn Skill) 的区别</p>
<ul>
<li>学习记录文件（Learn Skill）：全局的、永久的、抽象的知识规则，<strong>目的是避免重复犯错，积累经验</strong></li>
<li>会话记录文件（Session Tmp）：局部的、临时的、具体的工作状态，<strong>目的是用于跨会话的连续性</strong></li>
</ul>
<p>文件所在的位置也不同：</p>
<ul>
<li>学习记录文件（Learn Skill）：/.claude/skills/learned/jsonwebtoken-v9-migration.md</li>
<li>会话记录文件（Session Tmp）：/.claude/sessions/2026-01-20-auth-feature.tmp</li>
</ul>
<p>学习记录文件的例子：</p>
<ul>
<li>工具的描述要说明"何时使用"而不只是"做什么"</li>
<li>工具参数的设计中必填参数尽量少，可选参数提供默认值</li>
<li>件路径参数要说明是相对路径还是绝对路径</li>
</ul>
<p>会话记录文件的例子：</p>
<ul>
<li>创建了 <code>database_query</code> 工具的基础定义</li>
<li>在工具描述中明确说明"优先使用 simple_query，只有必要时才用 complex_query"</li>
</ul>
<h2>三、提高项目可维护性 - 检测评估 + 冗余代码清理</h2>
<h3>3.1、基于检查点的评估</h3>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-66.CQdfokTo_ZvQ2lj.webp" alt="检查点评估" /></p>
<p>使用基于检查点评估的方式，流程就是：<strong>开始 -&gt; 实现 -&gt; 验证</strong>这三步循环，每一个功能都由这三步来约束</p>
<ol>
<li><strong>开始</strong>：在功能实现之前，先运行一下开始步骤的指令，这样可以保证代码库的工作空间是干净的</li>
<li><strong>实现</strong>：这个时候就可以开始编写功能代码了，你自己编写或使用 AI 编写都可以</li>
<li><strong>验证</strong>：在这个功能完成的差不多了，或者已经编写一段时间的代码啦，可以运行验证指令，进行简单的代码评估，评估验证你这段时间写的代码如何，是否合格，是否符合要求规范</li>
</ol>
<p>🧩 那么我们来细细的说一下开始指令 ( /checkpoints create )</p>
<ol>
<li>执行/verify quick 的指令，只检查构建和类型的错误，这样执行起来快</li>
<li>执行 git stash 或 commit，保存当前的代码状态，让功能开始之前工作空间是干净的</li>
<li>执行<code>git rev-parse --short HEAD</code> 把 SHA 写入到日志文件 <code>checkpoints.log</code> 中</li>
</ol>
<p>🧩 接下来我们来说一下验证指令 ( /checkpoint verify )</p>
<ol>
<li>先从 checkpoints.log 中去除最近的 SHA</li>
<li>大模型自己根据<strong>具体情况调用 git diff 和运行代码相关测试的命令</strong></li>
<li>根据"<strong>关键指标</strong>"的要求输出报告，报告中要有：新增文件、修改文件、测试通过率、覆盖率等</li>
</ol>
<p>这种方式真的非常好，非常优雅，<strong>因为所有的流程不是强制自动化的，只是提供了最小的必要条件</strong>"SHA"。
至于如何获取新增和修改文件，测试和覆盖率这些指标，都是由模型自己来决定的，最大程度上保证了模型自主性，目前模型能力已经很厉害了，保持模型的自主性，我们未来可以用最小的改动代价换来最大的能力提升</p>
<p>🌴 该模式下使用到的文件 Command 和 Skill 为：</p>
<ul>
<li>commands/checkpoint.md</li>
<li>commands/verify.md</li>
</ul>
<h3>3.2、持续评估</h3>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-67.BiGKSPHk_MELBQ.webp" alt="持续评估" /></p>
<p>在持续评估的模式下，具体的流程为：</p>
<ol>
<li><strong>触发点</strong>：每 N 分钟或重大变更后运行</li>
<li><strong>运行点</strong>：运行完整的测试套件、构建状态、代码检查等，使用 Skill 或 Command</li>
<li><strong>运行结果</strong>：输出完整详细的检测报告</li>
<li><strong>判断结束点</strong>：根据输出的报告判断当前的代码是否合格，如果合格就结束，不合格的话就要进行修复</li>
<li><strong>修复点</strong>：对于报告中不合格的检测点进行修复，成功修复之后结束</li>
</ol>
<p>🌴 该模式下使用到的文件 Command 和 Skill 为：</p>
<ul>
<li>skills/verification-loop</li>
<li>commands/verify.md</li>
</ul>
<p>两种检测评估方式的区别：</p>
<ol>
<li>基于检查点评估的方式：适合用于具有明确里程碑的线性工作流程</li>
<li>持久评估的方式：适合长时间运行的会话</li>
</ol>
<p>🌟 <strong>所以决定因素是任务的性质，基于检查点评估方式适用于具有明确阶段的特性实现，持续性的评估方式适用于探索性重构或维护，这类工作是没有明确的里程碑和结束点的</strong></p>
<h3>3.3、清理冗余代码</h3>
<p>清理冗余代码的功能是使用一个子 Agent 和一个 Command，<strong>区别的话就是 Agent 更加详细完整一些，Command 指令轻便一点，直接一点</strong>：</p>
<ol>
<li>Command 指令：commands/refactor-clean.md</li>
<li>子 Agent 的设计：agents/refactor-cleaner.md</li>
</ol>
<p>目前 Command 指令更加清晰直接一些，我们接下来就分析整理一下这个命令书写的流程吧</p>
<pre><code># 重构清理

通过测试验证，安全识别并移除死代码：

1. 运行死代码分析工具：
    - knip：查找未使用的导出与文件
    - depcheck：查找未使用依赖
    - ts-prune：查找未使用的 TypeScript 导出
2. 在 .reports/dead-code-analysis.md 中生成完整报告
3. 按严重程度分类发现结果：
    - SAFE：测试文件、未使用的工具
    - CAUTION：API 路由、组件
    - DANGER：配置文件、主入口
4. 只建议安全删除项
5. 每次删除前：
    - 运行完整测试套件
    - 确认测试通过
    - 应用变更
    - 再次运行测试
    - 如失败则回滚
6. 展示已清理项目的汇总

在运行测试之前，绝不删除代码！
</code></pre>
<ol>
<li>先使用工具找出冗余代码：<code>knip</code>、<code>depcheck</code>、<code>ts-prune</code> 这三种工具</li>
<li>把结果写入到文件中，同时对结果进行风险分类</li>
<li>按照"完全流程"进行删除冗余代码：安全流程是在删除代码的前后都要运行测试，也就是 (测试 - 删除 - 测试）</li>
</ol>
<h3>3.4、代码地图 - 可信的上下文</h3>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-68.Di2JgpHv_5zQIU.webp" alt="codeMap" /></p>
<p>有代码地图的存在，其就可以作为 AI 或者开发者了解代码库的入口，AI 可以通过代码地图，使用较少的 Token 就可以了解项目的全局面貌。</p>
<p>代码地图里面的文档不要多，尽量保持精简。</p>
<p>🌴 该模式下使用到的文件 Command 和 Skill 为：</p>
<ul>
<li>agents/doc-updater.md</li>
<li>commands/update-codemaps.md</li>
<li>commands/update-docs.md</li>
</ul>
<h3>3.5、一点总结</h3>
<p>那我们总结一下吧，关于提高项目可维护性的四种方式：</p>
<ul>
<li>在适当干预下，两<strong>种验证方法（基于检查点评估和持续评估）足以避免大部分技术债务</strong>。让 Claude 完成任务后通过运行技能和 PostToolUse 钩子进行验证，有助于实现这一点。</li>
<li><strong>持续更新代码地图也有帮助，因为它记录了变更日志以及代码地图随时间的演变过程</strong>，这提供了除代码仓库本身之外的可靠信息来源。</li>
<li><strong>通过严格的规则，Claude 将避免创建杂乱的随机.md 文件，避免为相似代码生成重复文件</strong>，也不会留下大量废弃代码。可以考虑 rules 的方式，全局的创建文档的规则</li>
</ul>
<h2>四、子智能体的使用方式：循环验证调用 + 编排</h2>
<p>使用子智能体的方式会导致整个会话的上下文产生"中断"</p>
<p>子代理的存在是为了通过返回摘要而非全部信息来节省上下文。然而，编排器拥有子代理所缺乏的语义上下文。子代理只知道字面查询，不了解请求背后的目的或推理过程。摘要常常遗漏关键细节</p>
<blockquote>
<p>来自@ PerceptualPeak 的类比："你的老板派你去开会并要求你提供摘要。你回来后向他汇报了情况。十有八九，他会有后续问题。你的摘要不会包含他需要的所有信息，因为你没有他那种隐含的上下文。"</p>
</blockquote>
<p>所以目前更好使用子代理的方式有两种**：循环验证调用模式和顺序执行的编排器**</p>
<h3>4.1、循环验证调用</h3>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-69.B7Ft8Y1j_1VUaKs.webp" alt="循环调用" /></p>
<p>循环验证调用的流程：</p>
<ol>
<li>主智能体评估子智能体的结果</li>
<li>当结果不合格的话，主智能体根据评估结果提出新的检索任务</li>
<li>子智能体按照新的检索任务继续检索</li>
<li>循环最多 3 轮</li>
</ol>
<p><strong>在这种模式下，主智能体派发给子智能体的任务要"具体的问题 + 更广泛的目标"，让整体的检索面积更大，能检索到更多的结果</strong></p>
<p>🌴 该模式下使用到的文件 Command 和 Skill 为：</p>
<ul>
<li><strong>skills/iterative-retrieval</strong></li>
</ul>
<h3>4.2、编排智能体</h3>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-70.BIp1UEmN_flNj4.webp" alt="编排智能体" /></p>
<p>编排智能体的原则是：</p>
<ol>
<li>每个代理接收一个明确的输入、并生成一个明确的输出</li>
<li>输出成为下一阶段的输入</li>
<li>切勿跳过任何阶段 - 每个阶段都蕴含价值</li>
<li>在智能体之间使用/clear 命令以保持上下文的新鲜度</li>
<li>将中间输出存储在文件中（而非仅存在内存中）</li>
</ol>
<p>除了固定的四种功能编排好的子智能体调用流程，还可以自定义子智能体的调用顺序</p>
<pre><code>/orchestrate custom "architect,tdd-guide,code-reviewer" "Redesign caching layer"
</code></pre>
<p>🌴 该模式下使用到的文件 Command 和 Skill 为：</p>
<ul>
<li>commands/orchestrate.md</li>
<li>agents/architect.md</li>
<li>agents/code-reviewer.md</li>
<li>agents/planner.md</li>
<li>agents/security-reviewer.md</li>
<li>agents/tdd-guide.md</li>
</ul>
<h2>五、文章配置详解</h2>
<p>🌴按照核心功能拆分使用的话：</p>
<ol>
<li>跨会话共享内存
<ul>
<li>scripts/hooks/session-start.js：会话开始钩子</li>
<li>scripts/hooks/pre-compact.js：会话压缩钩子</li>
<li>scripts/hooks/session-end.js：会话完成钩子</li>
</ul>
</li>
<li>持续学习并更新记忆：
<ul>
<li>commands/learn.md</li>
<li>skills/continuous-learning</li>
</ul>
</li>
<li>提高项目的可维护性：
<ul>
<li>commands/checkpoint.md</li>
<li>commands/verify.md</li>
<li>skills/verification-loop</li>
<li>commands/refactor-clean.md</li>
<li>agents/refactor-cleaner.md</li>
<li>agents/doc-updater.md</li>
<li>commands/update-codemaps.md</li>
<li>commands/update-docs.md</li>
</ul>
</li>
<li>子智能体使用方式：
<ul>
<li>skills/iterative-retrieval</li>
<li>commands/orchestrate.md</li>
<li>agents/architect.md</li>
<li>agents/code-reviewer.md</li>
<li>agents/planner.md</li>
<li>agents/security-reviewer.md</li>
<li>agents/tdd-guide.md</li>
</ul>
</li>
</ol>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[编程 Agent 的工程实践：来自 OpenAI 与 Anthropic 的实战经验]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/agent-engineering-practice/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/agent-engineering-practice/</guid>
            <pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[从 OpenAI Codex 和 Anthropic Claude 的工程实践中提炼构建长期可靠运行的 Agent Harness 的核心经验。]]></description>
            <content:encoded><![CDATA[<p>相关的链接：</p>
<ul>
<li>OpenAI 的文章：https://openai.com/zh-Hans-CN/index/harness-engineering/</li>
<li>Anthropic 的文章：https://www.anthropic.com/engineering/harness-design-long-running-apps</li>
</ul>
<h2>一、OpenAI 的实战经验</h2>
<p>OpenAI 团队在尝试的一项实验是：<strong>构建并发布一个内部测试版的软件产品，该产品没有使用任何手动编写的代码</strong></p>
<p>需要完成这项任务，团队要为 Codex 构建出来一个可以长期可靠运行的 Agent Harness，也就是说软件工程团队主要工作不再是编写代码，<strong>而是设计环境，明确意图并构建反馈循环</strong></p>
<p>让 Codex 可以在几周的时间内交付百万行代码的项目，并且这个项目已经被数百名内部用户使用</p>
<blockquote>
<p>[!NOTE]
说明该项目不仅仅是为了短时间内堆积代码量，而是希望可以让用户正常使用并被认可的</p>
</blockquote>
<p>我梳理了一下这个编码 Agent 运行空间 (Harness) 内的核心板块和输入的上下文</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/PzFubmMhJojjN7xZlL9cD7sJncc.UcUAztnG_ZRDvq0.webp" alt="OpenAI Codex Harness 架构" /></p>
<p>🌴 <strong>第一步：三层代码审查</strong></p>
<p>工程团队最早注意到的就是代码审查的模块，在 Codex 完成需求之后，要指示 Codex 完成三层代码审查，</p>
<p>三层审查分别是：自身审查，本地代码审查 Agent、云端代码审查 Agent，</p>
<p>只有所有的审查都通过之后才可以进行下一步，否则就借助相应的工具，将审查结果这类上下文再次注入回 Codex 进行修改</p>
<p>🌴<strong>第二步：人工质量检查 (Human QA)</strong></p>
<p>随着 Codex 编写代码的速度加快，整个项目的限制节点变为了人工质量检查的部分，</p>
<p>为了加快这一节点的操作，OpenAI 团队使用 Chrome DevTools 协议集成给 Codex，让 Codex 可以拥有处理 DOM 快照，屏幕截图和导航的能力，这让 Codex 直接拥有分析 UI 的能力</p>
<p>🌴<strong>第三步：日志检测和性能优化</strong></p>
<p>OpenAI 团队同时也将运行日志和性能指标这类上下文也输入给 Codex，</p>
<p>这样当出现"性能优化的任务时"，Codex 可以拥有相应的上下文进行分析，以此来合理优化项目中的性能问题，而不是仅仅靠对于代码结构的感知，Codex 可以实践 -&gt; 观察-&gt; 修改</p>
<p>🌴<strong>第四步：代码文档库的构建</strong></p>
<p>一个代码库的详细文档是非常庞大的，不能一次注入给 Codex，这样上下文的利用会非常低效，借助 Skill 规范中的"渐进式披露"的概念，对于整个文档库，采用目录 - 文件的形式传递给 Codex</p>
<p>OpenAI 团队非常巧妙的使用 AGENTS.md 当作文档库的目录，里面存放相应的文档路径和简单的介绍</p>
<p>将是否读取和读取什么完全交给 Codex 来决定，在这种设计下，上下文的利用会非常高效，那么文档库也可以发挥编码指导的作用</p>
<p>具体的文档库细节：OpenAI 团队将计划文档作为"一等公民"，还可以有设计文档，架构文档，质量文档</p>
<p>里面有一个很重要的细节，一个项目中的功能需求实现，是可能会经过团队成员进行讨论确定的，如果这份"讨论信息"没有被落到文档库中给 Codex 读取，这类代码库原本拥有的上下文在 Agent 的运行空间中就是不存在的</p>
<p>那么 Codex 获取到的信息是不完整的，极可能在长期的运行中偏离正确方向</p>
<p>所以对于这类"决策信息"，要创建相应的工具让 Codex 可以获取到，这也是 OpenAI 该团队的设计目标</p>
<p>代码结构在不断的变化，那么文档库是需要经常更新和维护的，所以 OpenAI 团队在流程中设计了定时维护文档的功能节点，实现方式也非常的简单，就是运行一个相应的"文档维护"Agent 来扫描和清理文档</p>
<p>🌴<strong>第五步：代码库结构性规则</strong></p>
<p>这一部分是一些代码库的规范，用于保证代码库不会随着时间推移变得混乱和失控，这仅仅依靠上面的文档库是无法完全做到的，文档库对于 Codex 更多的是引导作用，而这一步的结构性规则偏向于约束作用</p>
<p>例如：当 Codex 要增加一个功能的时候，先从什么地方开始增加，要考虑哪一层的结构，这个依赖于顺序规则</p>
<p><strong>类型-&gt; 配置 -&gt; 存储库 -&gt; 服务-&gt; 运行时-&gt;用户界面</strong></p>
<p>该规则的校验方式是依赖于自定义的代码检查器的执行（这个代码检查器也是由 Codex 编写的）</p>
<p>代码检查器执行的时候，会检查到代码库中的编写错误，该工具会将错误信息传递给 Codex</p>
<p><strong>🍺 一点总结</strong></p>
<p>从这个实践中可以发现，OpenAI 团队在将<strong>成熟的软件工程开发经验</strong>用于构建编码领域的 Agent 运行空间中去，正如他们说的那样：</p>
<blockquote>
<p>显而易见的是：软件开发仍然需要严谨的纪律，但这种严谨更多地体现在框架搭建而非代码本身。用于保持代码库一致性的工具、抽象和反馈循环变得越来越重要</p>
</blockquote>
<p>所以对于构建长期稳定运行的 Agent，<strong>最大的挑战其实是：设计环境，反馈回路和控制系统，而解决方案我们可以从这些实践中总结出来一些</strong></p>
<ol>
<li>对于 Agent 完成任务中的任何步骤，需要提供执行反馈的功能，以此将执行结果输入给 Agent，实现反馈回路</li>
<li>从一般规律中找特殊，如果该 Agent 需要服务于具体的场景，那么约束控制是有效的</li>
<li>为 Agent 提供更多的有效上下文，在其运行环境中，最佳实践目前是"文档渐进式加载"</li>
</ol>
<h2>二、Anthropic 的实战经验</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/HQPXbkp3BoEsLrxM8wicr9Kzn2g.DYa9CVln_Z1tkRI9.webp" alt="Anthropic 初始架构" /></p>
<p>在构建能够支持编码智能体长时间运行框架的时候，Anthropic 团队使用的是<strong>任务初始化 Agent+ 编码智能体</strong>的简单的两层多智能体架构设计，随着运行时间的增加和任务的复杂度提升，出现了两种常见的故障模式：</p>
<ol>
<li>随着上下文窗口逐渐填满，模型会失去连贯性，同时部分模型还会表现出"上下文焦虑"，尤其是 Sonnet 4.5</li>
<li>在设计自我评估的模块时，当要求 Agent 评估自己生成的作品时，其往往会自信的给予高度赞扬，这很容易导致评估模块失效</li>
</ol>
<p>🌴 对于第一个问题，Anthropic 团队的解决方法是：<strong>上下文重置</strong></p>
<p>完全清除上下文（不仅仅是依赖上下文压缩），并启动一个新的 Agent，同时配合结构化的交接机制（该机制会传递前一个 Agent 的状态和后续步骤）</p>
<p>🌴对于第二个问题，<strong>将评估任务使用的 Agent 与执行任务使用的 Agent 分开</strong></p>
<p>也就是说不要在同一个 Agent 中即赋予任务执行，也赋予任务评估，虽然这种分离本身不能立即消除"评估宽容"</p>
<blockquote>
<p>[!NOTE]
"评估宽容"：评估 Agent 依旧是一个 LLM，它会倾向于对 LLM 的生成的输出给予较高的评价</p>
</blockquote>
<p>这种分离的方式，是目前最有效的解决方法啦，至少可以有效降低评估与执行集中在同一 Agent 中所带来的失效风险</p>
<p>接下来，Anthropic 团队在原有框架的基础上，再次进行了改进，构建了一个三种 Agent 的系统</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/NwPvbdrZzobMevxLaUycboiunXf.DRKihgEL_ZVNQn9.webp" alt="Anthropic 三 Agent 架构" /></p>
<ol>
<li>规划器：它能够接收 1-4 句话的简单提示，并将其扩展为完整的产品规格说明。我要求它在范围方面设定得更远大一些，并专注于产品背景和高层技术设计，而不是具体的实现细节</li>
<li>生成器：以循环执行的方式工作，每次从需求清单中选取一个子任务执行</li>
<li>评估器：使用 Playwright MCP 模拟用户操作，逐个点击运行中的应用程序，测试 UI 功能、API 端点和数据库状态，<strong>并且基于一套标准进行评分</strong></li>
</ol>
<p>这套设计中有很核心的两点实践经验可以借鉴：</p>
<ul>
<li><strong>在每次子任务执行前，生成器和评估器会一起协商一份开发契约：在编写任何代码之前，就这一部分工作的完成标准达成一致</strong>，之所以有开发契约是因为在规划器书写需求清单的时候，是有意写的比较概括的，Anthropic 团队希望通过这一步来弥合用户需求和可测试之间的差距</li>
<li><strong>🌟 Agent 之间的通信使用文件来进行</strong>，一个 Agent 写入一个文件，另外一个 Agent 读取该文件，响应内容也可以写入该文件，通过文件的读取写入来进行通信</li>
</ul>
<p>关于上面提到的评估器的标准的构建，Anthropic 的方式也非常值得学习</p>
<p>团队要为前端的实现进行评估，大家知道美学是无法完全使用分数来衡量的，每个人的品味都不一样，一千个读者眼中就有一千个哈姆雷特</p>
<p>Anthropic 给出的解决方案是:</p>
<blockquote>
<p>我们可以通过编码设计原则和偏好的评分标准来提升设计水平。"这个设计美观吗？"很难给出一致的答案，但"它是否符合我们对优秀设计的原则？"则为 Claude 提供了一个具体的评分标准</p>
</blockquote>
<p>也就是说，问题从"这个设计漂亮吗？"，变为了"这个设计符合我们的设计原则吗？"，举一个例子：</p>
<ul>
<li>问"这篇文章写得好吗？"，这个很难回答，因人而异</li>
<li>问"这篇文章是否结构清晰、论据充分、语言流畅"，这种情况下就有了具体的评估标准</li>
</ul>
<p><strong>🌟 将"模糊的主观判断"变为"可操作的评分标准"</strong></p>
<p>Anthropic 团队关于前端设计标准分为四项</p>
<ol>
<li>设计质量：设计是否感觉像是一个连贯的整体，而不是各个部分的简单堆砌？优秀的设计意味着色彩、字体、布局、图像和其他细节相互融合，共同营造出独特的氛围和风格</li>
<li>原创性：是否存在自定义决策的痕迹，还是仅仅使用了模板布局、库默认设置和人工智能生成的图案？一位优秀的设计师应该能够识别出精心设计的创意。未经修改的现成组件——或者像白色卡片上叠加紫色渐变这样的人工智能生成痕迹——都无法体现原创性</li>
<li>工艺：技术执行：排版层级、间距一致性、色彩和谐、对比度。这考察的是能力，而非创意。大多数合理的实现方式默认都能达标；失败则意味着基本功薄弱</li>
<li>功能性：可用性独立于美观性之外。用户能否理解界面功能，找到主要操作，并在不猜测的情况下完成任务？</li>
</ol>
<p>对于 Claude 模型来说，其本身在工艺和功能性上面表现就非常出色，我们应该注重设计质量和原创性</p>
<p>Anthropic 团队对于这个框架不断的进行迭代，最终的方案为：</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/LYyDbmNFko3eDaxQ19TcHHxfngf.IXcPg0SB_Z14nEBr.webp" alt="Anthropic 最终方案" /></p>
<p>因为模型的升级，最终方案使用的是 Opus4.6，所以之前的设计方案有一些被移除啦</p>
<ol>
<li>移除任务拆分的功能，不需要小任务多次循环执行，Opus4.6 完全可以处理这种任务整体执行</li>
<li>移除了开发契约的功能，评估器直接看最终产物，不需要进行开发协商</li>
</ol>
<p><strong>🍺 总结：对于 Harness 的设计，不会是一成不变的，随着模型基础能力的提升，整个 Harness 是需要做删减和增加的。</strong></p>
<blockquote>
<p><strong>一个 Agent 的优化，不仅是改变模型型号这么简单，而是一些相应的工具和模块会成为模型的阻碍点，是需要删除的，</strong></p>
</blockquote>
<p><strong>当然对于 Harness 的组合设计空间并不会缩小，它会不断的扩展，而大模型应用开发工程师真正的乐趣或许在于不断寻找下一个新颖的组合</strong></p>
<h2>三、从开发 Agent 角度思考 Harness</h2>
<p>1、要构建 Agent 的审查模块，Agent 的输出都可以经过审查模块，如果审查不通过就将审查结果注入回上下文，继续执行 Agent，直到审查结果通过，审核模块的 Agent 最好和执行 Agent 分开，是两个完全不同的上下文环境</p>
<p>2、Agent 之间的消息传递或者工具和 Agent 之间的消息传递，可以考虑简单有效的方法，使用 md 格式的文件来传递消息，发出消息的 Agent 写入文件，收到消息的 Agent 读取文件</p>
<p>3、审查模块的 Agent 是需要一份"审查规范"的，也就是说什么情况下执行 Agent 的结果是通过的，这份规范里面包含评判标准和通过条件，</p>
<p>就像老师批改试卷一样，每个老师都有一份相同的评分标准存在，同时考试及格的条件是 60 分，</p>
<p>关于这份审查规范的创建是开发者的任务之一，开发者要主动将主观的判断标准转变为客观的，就像 Anthropic 博客中说的一样，不要纠结于"这个结果好不好"，而是专注于"好的结果标准是什么"，这个标准就是审查 Agent 需要的审查规范</p>
<p>4、要学会简单有效的原则去构建 Agent，对于构建出来的 Harness 要明白它是动态的，随着模型能力的升级是会不断调整的，学会去感知模型升级给你构建的 Harness 会带来什么样子的变化，这里可以使用 Agent 评估这一方法去辅助你感知，而不是仅仅依靠你的感觉</p>
<p>5、目前最佳的多智能体的核心架构设计方案，或许是：<strong>规划 - 执行 - 评估</strong></p>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[给 Agent 接入 Browser use 的设计思路]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/agent-browser-use/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/agent-browser-use/</guid>
            <pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[从 CUA、DOM CUA 到 Playwright，梳理 Agent 控制浏览器的核心实现思路与 Codex、Open-Browser-Use、ActSpace 三种架构设计对比。]]></description>
            <content:encoded><![CDATA[<p>让 Agent 控制浏览器，其实有很多的实现方式，你可以在应用中内嵌一个浏览器，像 cursor、codex 那样，在应用中就可以打开网页，内部就可以使用 tool 工具定义实现，然后进行控制读取，但是这种方式是无法继承用户完整的浏览器权限的，无法完整模拟用户去操作浏览器</p>
<p>所以我更偏向使用浏览器插件作为中转，来控制用户的原本的浏览器，插件可以实现原始的 Chrome API 接口读取和操作 Tab 页面等方式，可以使用 CDP 提供的 API 来模拟人类使用浏览器，点击，下载，查看，输入等方式</p>
<p>Agent 操作浏览器是一个很复杂的事情，有很多细节是值得慢慢学习的，我这次梳理只是让自己对于实现 Agent 控制浏览器有一个整体大概的理解，但是对于 CDP、CUA、还有 Playwright 的具体使用和细节，我需要更多的时间在实践中慢慢理解，所以本篇文章就作为一个"引子"吧，让大家可以更好的去探索学习 Agent 操作浏览器这件事</p>
<p>调研资料：</p>
<ul>
<li>《open-browser-use 项目》：https://github.com/iFurySt/open-browser-use</li>
<li>《Actspace 的项目》：https://github.com/WakeUp-Jin/actspace-agent</li>
<li>《Notch Agent》：https://github.com/Puggo1145/Notch-Agent</li>
</ul>
<p>::github{repo="iFurySt/open-browser-use"}</p>
<h2>一、Browser use 核心实现思路</h2>
<p>我们想要实现让 Agent 可以操作浏览器功能之前，首先要了解模拟人类使用浏览器的一些原语。</p>
<blockquote>
<p>[!NOTE]
原语：观看确认位置、鼠标点击、鼠标双击、鼠标移动、键盘输入，键盘按键，下载，拖拽、网页滚动</p>
</blockquote>
<p>借助这些原语，Chrome DevTools Protocol 提供相应原语操作浏览器的 API，只有理解这第一层，那么 Agent 操作浏览器的功能实现起来就不再没有方向啦。</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/browser-1.BRUn318B_1texL0.webp" alt="Browser use 核心思路" /></p>
<p>采用 CUA 的方式来实现让 Agent 操作浏览器，操作方式我们可以借助 CDP 提供的 API 来封装自己的函数。</p>
<p>例如：我需要点击网页的按钮，那么就可以封装一个点击函数，里面执行 CDP 中相应的 API 就可以</p>
<p>但是这里最关键的是一切操作的源头"看"，Agent 怎么知道点击什么按钮？，这个按钮的位置在哪里？</p>
<p><strong>所以 CUA 中最核心的是：截图来确定坐标，通过确定下来的坐标就可以执行相应的操作事件，所以对于这一步，我们需要模型有多模态的能力，拥有识图的能力</strong></p>
<p>那么如果模型没有识图能力的话，或者说识图能力比较弱，那么我们可以使用 DOM CUA 的方式来实现让 Agent 操作浏览器，DOM CUA 的方式和 CUA 不同的是，<strong>DOM CUA 不借助截图分析来确定起始操作的位置，而是通过 DOM 来确定操作的位置，并且通过 node_id 执行相应的操作</strong></p>
<p>DOM CUA 中关键的方法是：get_visible_dom 方法，这个会获取网页上一切可见的 DOM 元素，并且以 JSON 的格式将结果返回给 Agent，这样 Agent 就可以获取到元素的 node_id 用来执行相应的操作啦</p>
<p><strong>🎃一个小提示：DOM CUA 获取全部 DOM 元素的方法，内部调用的是 CDP 的原生的 API，而其他的操作内部调用的是已经封装好的 CUA 的函数</strong></p>
<p>除了 DOM CUA 的方式，我们还可以使用成熟的浏览器操作框架 Playwright，它里面有很多封装好的完整安全的执行流程，同时它也可以获取到 CSS 元素来作为操作条件，会比 DOM CUA 细很多</p>
<p>例如：它可以实现等待的操作、也可以使用元素过滤查询，可控性很强</p>
<pre><code>-----执行等待操作-------
CDP（你自己来）：
  发 Runtime.evaluate("document.querySelector('.result')")
  → 如果元素还没加载出来 → 返回 null → 失败
  → 你得自己写 while 循环 + sleep + 重试 + 超时处理

Playwright（自动帮你等）：
  wait_for(selector=".result", state="visible")
  → 内部自动轮询、检查状态、处理超时
  → 只在元素真正可见后才返回
</code></pre>
<h2>二、如何将核心接入 Agent</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/browser-2.BImbOYXA_Z1cMS6Y.webp" alt="接入 Agent 的方式" /></p>
<p>目前可以考虑三种比较不错的方式：</p>
<ol>
<li>内嵌 Tool 工具：将这些函数以工具列表的方式提供给 Agent 调用</li>
<li>MCP 服务器：以 MCP 的方式提供给 Agent，设计好暴露出去的资源函数</li>
<li>Skill+Cli 方式：将这些函数封装称为一个 cli 工具，借助 skill 作为工具"使用指南"提供给 Agent 调用</li>
</ol>
<h2>三、Browser use 完整架构</h2>
<p>我们一共分析三种应用的架构设计，每一种的侧重点都不同，应用场景也是不同的，值得多思考</p>
<ol>
<li>Codex 的 Browser use</li>
<li>Open Browser use</li>
<li>ActSpace 的 Browser use</li>
</ol>
<p><strong>1、首先我们梳理的是 Codex 的实现设计</strong> Codex 的实现会将大部分的逻辑实现在 Browser-client.js 中，该文件近 2700 行代码，非常的复杂，而使用 rust 实现的 extension-host 只是简单的做消息的转发的中继器的角色，下图就是数据链路的传递。</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/browser-3.CHHlqYO8_FqYYs.webp" alt="Codex Browser use 架构" /></p>
<p>Codex 的实现中，有一些交互上面的小细节非常值得参考借鉴，第一个是：鼠标的移动是有起始位置的，会丝滑的过渡点击，第二个是：Agent 创建的 Tab 页面和用户创建的页面是分开的，有清晰的样式可以区分</p>
<p><strong>2、其次是 Open-Browser-Use 的架构分析实现</strong> 该插件的实现，重点在"open"上面，所以对于调用方会做的非常全面，可以 skill 的 cli 方式调用，也可以 MCP 直接连接，甚至你开发的 Agent 的话也可以直接 SDK 接入，open-browser-use 的实现大部分业务逻辑是放在 go 实现的客户端上，里面同时存在和浏览器插件通信的文件进程</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/browser-4.Bhvoi5tU_vyG5g.webp" alt="Open Browser use 架构" /></p>
<p><strong>3、对于 ActSpace 的架构设计分析</strong> 我借鉴了上面两个优秀的设计思路，为了更好的和 actspace 项目内部的逻辑结合在一起，使用文件的方式直接嵌入到项目源码中去，由这个 browser-tool 文件来定义提供什么浏览器操作给 Agent，由它来做第一道安全的把关，整体的文件没有 codex 那么重，很轻量，文件只是简单的将消息转发和工具提供的职责，没有大量的业务。</p>
<p>为了保证提供 Skill+Cli 的插件访问形式，我将大量的业务放在 Go 实现的 cli 上面啦，里面是核心的浏览器操作实现指令，同时连接插件的进程也是在 cli 中实现的。</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/browser-5.NyzZEy9Y_ZrsEpG.webp" alt="ActSpace Browser use 架构" /></p>
<p>ActSpace 的实现中，有一个小细节，因为担心 tool 工具定义的实现，将浏览器操作的工具提供给 Agent，我担心参数太复杂，工具描述不能清晰完整的介绍，导致 Agent 调用的时候出现大量的错误，所以我提供啦一个 browser_help 的命令，执行这个命令之后会返回完整的指令介绍还有参数描述，极大的提高 Agent 调用浏览器控制的正确性</p>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[ActSpace 评估模块的设计思路]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/actspace-eval-design/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/actspace-eval-design/</guid>
            <pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[依靠感觉和经验可以让你走起来，但要走的更远，我们需要一个构建优化的"理由"——Agent 评估模块的设计实践。]]></description>
            <content:encoded><![CDATA[<p>在构建 Agent 的时候，我们首先会更多从感觉出发，或者说从经验出发</p>
<p>感觉添加这个工具会有效，这样处理上下文会有效，这样编排提示词应该没有问题</p>
<p>这种方式对于一位经验丰富的大模型应用工程师来说，构建出来的 Agent 至少会是合格的。</p>
<p>但是下一步呢？如果我想要让这个 Agent 变得更好呢，我们会陷入一种迷茫的状态，可能会再去看看其他的构建思路和经验，但是远水解不了近渴，别人需求环境下构建的经验不一定适用你目前的情况</p>
<p>主要的问题在于：我们不知道当前的 Agent 到底差在哪里，没法去量化 Agent 执行的细节</p>
<p><strong>依靠感觉和经验可以让你走起来，但是要走的更远，我们需要一个构建优化的"理由"</strong></p>
<p>我们可以把思绪拉到 Agent 评估上面，从现有的数据中构建出来自己的评估数据集，使用公开的数据集进行测试，然后观察执行链路的问题，根据实际执行环境去评估上下文的质量。</p>
<p>Agent 评估可以帮助我们确定 Agent 开发方向，同时也可以提供有力的数据给我们，让每一次构建可以更果决</p>
<p>一个好用的 Agent 背后，一定是存在一个合格的 Agent 评估模块的。</p>
<p>我目前正在构建 ActSpace 这个桌面端 Agent，它的 Agent 评估模块的设计思路我整理出来，希望可以给大家提供一些参考</p>
<p>调研分析资料：</p>
<ul>
<li>《Agent 评估体系的构建》：https://mp.weixin.qq.com/s/3VqbQzT9ruRVP9B4jlFAEg</li>
<li>《SWE-bench Lite》：https://www.swebench.com/lite.html</li>
<li>《ActSpace 代码库》：https://github.com/WakeUp-Jin/actspace-agent</li>
</ul>
<p>::github{repo="WakeUp-Jin/actspace-agent"}</p>
<h2>一、ActSpace 的 Agent 评估模块设计</h2>
<p>我们先从这个模块的输入开始理解吧，这样可能会更容易一些，我设计的这个评估模块总共有三种输入核心：行为评估数据集、内部数据集、外部公开数据集</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/actspace-eval-design.CZ-bm4D4_2uBfok.webp" alt="ActSpace 评估模块设计" /></p>
<p>对于行为评估数据集，主要是用于评估 Agent 的执行链路和上下文质量、</p>
<ul>
<li>执行链路：是否调用必须的工具，调用的顺序是否正确，失败之后的处理等</li>
<li>上下文质量：工具结果是否正确进入下一轮、压缩之后是否丢失了任务目标，工具错误是否导致上下文被污染等</li>
</ul>
<p>对于内部评估数据集，主要是用于与公开数据集区分开来，防止优化过拟合，这个主要是评估 Agent 的执行结果，值得详细聊的是，<strong>数据集的建立方式：失败案例中总结和同场景 Agent 的优秀数据集内化。</strong></p>
<p>评估的流程是，Agent 会根据用户输入，对于代码库进行功能的开发或者 Bug 的修复，代码编写完成之后，会执行测试文件，如果所有的测试用例都通过，就表示 Agent 本次任务执行成功</p>
<p>所以评估器的核心是：代码库拥有完整的测试文件和执行测试命令</p>
<p>对于公开的评估数据集，我们这边只负责执行 Agent cli，同时收集一些信息放入到 prediction 文件里面去，之后评估器使用的是官方库自带的 Harness 框架</p>
<p>评估的核心思路：会在相同的代码库中，相应的 commit 分支执行 git apply 将产生的 diff 代码应用到代码库中，然后执行相应的测试命令，测试用例都通过就表示 Agent 修改成功</p>
<blockquote>
<p>[!NOTE]
和我们的内部数据集评估的方法是差不多的，只不过公开评估数据集的方法更完整，会保证环境的统一性，公开评估数据集使用的是 SWE-bench Lite</p>
</blockquote>
<pre><code>//prediction.json 文件
{
  "instance_id": "django__django-11099",
  "model_name_or_path": "your-model-or-agent-name",
  "model_patch": "diff --git a/... b/...\n..."
}
</code></pre>
<p>图中的 Agent cli 是我将 ActSpace 中的核心 Agent 模块封装成为了 cli 命令，这样方便调用，同时测试集的执行环境统一是在 Docker 容器里面的，这样会保证本地环境的安全性</p>
<p>在 cli 执行完成之后，会有一个运行后处理器，这个是用来做数据整理的，将 Agent cli 输出的结果整理成为评估模块需要的各种格式</p>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[让 Agent 从被动变为主动：定时任务和 KAIROS 模式]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/agent-kairos-mode/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/agent-kairos-mode/</guid>
            <pubDate>Tue, 14 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[从定时任务到 ClaudeCode 的 KAIROS 模式，探索让 Agent 从交互式变为常驻后台运行的设计思路：Sleep 工具与 tick 事件驱动。]]></description>
            <content:encoded><![CDATA[<h2>一、定时任务</h2>
<p>设计定时任务，让 Agent 到点就执行，将执行结果发送给我们，这个也是一种让 Agent"主动"的方式之一，只不过是由定时任务驱动的，下面是一个简单的定时任务的设计思路，主要是核心设计：<strong>Agent 生产，轮询调度器消费</strong></p>
<p>给 Agent 添加定时任务，主要是三种核心设计：</p>
<ol>
<li>定时任务的存储：一个 JSON 文件存储定时任务，由轮询调度器读取来执行定时任务</li>
<li>轮询调度器：每秒读取定时任务 JSON 文件，达到条件的任务就开始执行</li>
<li>三种定时任务工具：创建、查询、删除这三种工具提供给 Agent 使用</li>
</ol>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/W0jQbrcIsozdoCxQTAjcADeInpc.BfJz6I38_Z1wxrNm.webp" alt="定时任务设计" /></p>
<p>定时任务的存储可以采用 JSON 文件的格式，里面的任务对象时间表示使用的 cron 格式</p>
<blockquote>
<p>[!NOTE]
Cron 时间格式可以轻松的表达一次性任务和循环任务，非常的方便，并且格式统一方便调度执行</p>
</blockquote>
<p>任务的创建有两种方式：用户输入和/loop 指令</p>
<ul>
<li>用户输入：用户输入由模型解析任务的时间和任务指令，调用相应的创建任务的工具，将任务写入到 JSON 文件里</li>
<li><code>/loop</code> 指令：这个方式会比用户输入更加精确，会约束好一个完整的时间解析规则和指令后面的输入一起注入给 LLM，并且任务创建之后会立即执行一次，并且该指令创建的任务都是循环任务</li>
</ul>
<p><code>/loop</code> 的解析规则这里详细说明一下吧：</p>
<ol>
<li>前置间隔：指令的第一个空格分隔匹配出来的数字就是 cron 的循环时间，例如：<code>/loop 30m check deploy</code></li>
<li>尾部的 every：如果输入结尾有 every N，那么 N 就表示循环的时间，例如：<code>/loop run tests every 5 minutes</code></li>
<li>默认规则：如果上面两种规则都没有匹配到，那么循环时间默认就是 10m</li>
</ol>
<p>定时任务的文件存储的内容可以参考下面这个对象：</p>
<pre><code>{
    "tasks": [
      {
        "id": "a1b2c3d4",
        "cron": "*/5 * * * *",
        "prompt": "检查部署状态",
        "createdAt": 1712830000000,
        "lastFiredAt": 1712830300000,
        "recurring": true
      }
    ]
  }
</code></pre>
<p>关于调度器的读取，如果你觉得每秒都要读取文件带来的 IO 开销影响性能，可以考虑缓存，<strong>每秒读取缓存，每 5 秒重新读取文件</strong></p>
<h2>二、KAIROS 模式</h2>
<p>在 ClaudeCode 设计思路中，有一个功能非常有意思，叫做<strong>KAIROS</strong>，是让 ClaudeCode 从交互式转变为常驻后台运行，让 Agent 从之前的被动交互，变为了主动运行，这里的有一些设计思路非常有意思，我们一起来学习解读一下</p>
<blockquote>
<p>Kairos 源自古希腊语，是一个关于时间的哲学概念，指代恰好的时机，关键的瞬间</p>
</blockquote>
<p>将 Agent 处理的所有任务统一放入到队列中去，我觉得这样可以让用户"单线程"的专注处理任务，</p>
<p>这一点的处理我很喜欢，在同一个会话中，Agent 后台运行的助手应该以不打断我的思路为前提，主动的去处理一些任务，并且 KAIROS 也指代"时机"这个意思，在恰当的时候去主动运行</p>
<p>队列的任务是有优先级的，并不是按照插入顺序取出，而是按照优先级来取出，用户输入的优先级是最大的</p>
<p>并且 KAIROS 模式的持续运行，不是简单的通过代码的 while 循环控制的，而是通过事件驱动的，通过上下文中的 tick 消息来控制的，非常灵巧</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/Nq5kb6T9Eoc56bxFuzqcoFjinjh.CTiy31f3_ZfMoTi.webp" alt="KAIROS 模式设计" /></p>
<ol>
<li>每一次 Agent 运行，首先从队列中按照优先级取出任务，判断任务类型是用户输入还是 KAIROS 模式的 tick 任务</li>
<li>任务是用户输入的话，就进入到正常的任务处理流程，和之前的方式一样，Claude 模型调用相应的工具，对上下文进行推理来完成用户的任务</li>
<li>任务是 tick 的话，那么更换系统提示词，使用 kairos 模式专属的提示词，Agent 根据实际情况有两种表现：执行任务和进入睡眠</li>
<li>KAIROS 模式下执行任务，像正常模式下一样，根据上下文推断执行任务，例如：跑测试、探索不熟悉的代码块、做小重构</li>
<li>有意思的是 KAIROS 模式下进入睡眠，当模型根据上下文推理得到目前暂时没有任何任务需要处理，就会主动进行"睡眠状态"，睡眠多久也是由模型自己决定的，这个的实现是通过一个 sleep 工具来实现的，在"睡眠状态"，可以随时被用户输入打断，用户输入在整个任务执行队列中是"一等公民"</li>
<li>无论是用户输入的任务执行完成，还是睡眠状态结束，都表示一轮调用结束，调用结束之后，会有一个判断的流程</li>
<li>判断流程就是根据队列的状态是否为空判断，如果队列不为空，那么就什么都不做，正常进行队列的下一轮执行，如果队列为空，那么就向队列中添加一条 tick 消息，以此驱动 KAIROS 模式下 Agent 的持续运行</li>
</ol>
<p>KAIROS 的实现有很多巧思，我这里重点梳理两点我自己很喜欢的设计</p>
<p>🌴<strong>第一点：睡眠状态的实现</strong></p>
<p>相比定时任务的实现，固定一段时间之后运行，例如：固定每 30 分钟之后启动运行一次这种方式，</p>
<p>我始终能感受到有点牵强，在这种设计下实现的"Agent 主动"，其实本质没什么说服力，也是用户主动设定的运行时间，感受下来还是有点被动的味道</p>
<p>但是 ClaudeCode 的设计中，**将什么时候睡眠，睡眠多久完全交给模型自己，**用户只负责给 Agent 开机</p>
<ul>
<li>在系统提示词中添加判断条件："当发现没有任务可做的时候，可以调用 sleep 工具进入睡眠状态"，将睡眠时机交给模型自己决定</li>
<li>在工具参数中添加 duration 参数：sleep 工具要定时多久使用参数来控制，将睡多久通过工具参数交给模型控制</li>
</ul>
<p>这种设计思路下，"主动运行"的味道更浓了一些，比定时任务赋予给 Agent 的权限更大了</p>
<p>sleep 工具定义的代码：</p>
<pre><code>export const SleepTool = buildTool({
    name: 'Sleep',
    description: '等待指定时长，用户可随时中断',
    inputSchema: z.strictObject({
      duration_ms:z.number().nonnegative().int().describe('睡眠时长（毫秒）')
    }),
    interruptBehavior: 'cancel',
    async call({ duration_ms }) {
      await new Promise(resolve =&gt; setTimeout(resolve,duration_ms))
      return { data: { slept_ms: duration_ms } }
    }
  })
</code></pre>
<p>🌴<strong>第二点：tick 消息的使用</strong></p>
<p>tick 是 KAIROS 模式下的<strong>事件驱动循环</strong>的触发源，该模式可以持续运行的原因是 Agent 每一次任务完成之后，都可能向任务队列中添加 tick，这样任务队列中会一直存在 tick，那么 KAIROS 模式下的 Agent 就可以不断的循环启动</p>
<p>tick 本质上就是一条消息，里面加一个动态时间变量</p>
<pre><code>&lt;tick&gt;14:20:15&lt;/tick&gt;
</code></pre>
<p>加入到模型上下文中的时候，是一条 user 消息</p>
<pre><code>{"role":"user","content":"&lt;tick&gt;14:20:15&lt;/tick&gt;"}
</code></pre>
<p>KAIROS 模式的设计，我觉得可以作为 Agent 主动运行的实现范式之一，核心思路是：<strong>Sleep 睡眠工具和 tick 事件驱动</strong></p>
<p>这种方式比定时任务更加灵活，实现也足够优雅，开发难度会比定时任务大一点，主要是在队列状态的维护，如果你希望 Agent 的运行更主动灵活一些，那么这种设计是值得一试的</p>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[Agent Bash 工具工程化：后台运行与沙盒权限设计]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/agent-bash-engineering/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/agent-bash-engineering/</guid>
            <pubDate>Sun, 12 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[从后台挂载、增量读取、事件推送到 macOS sandbox-exec 三层沙盒——Agent Bash 工具从 demo 到线上级的关键两环。]]></description>
            <content:encoded><![CDATA[<p>在上一篇文章中，我们对于 Bash 工具的简单设计做了一个整理，这种思路设计出来的 Bash 工具，只能跑一跑 demo 或者在项目前期的构建阶段"性价比很高"，但是如果要追求线上级的 Agent 的运行稳定，那么 Bash 工具最重要的两环是必不可少的：<strong>后台运行和沙盒设计</strong></p>
<p>参考分析资料：</p>
<ul>
<li>《上一篇：Bash 工具实现和安全设计》</li>
<li>《Anthropic 的轻量级沙箱工具》：https://github.com/anthropic-experimental/sandbox-runtime</li>
</ul>
<h2>一、上下文管理 - 后台运行</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/agentbash1.CNKGD4XT_Zyi3CR.webp" alt="Bash 工具后台运行与增量读取设计" /></p>
<p>Bash 工具是用来执行终端命令的，有一些项目启动命令，会持续执行很久，如果没有 blockMs 前台执行时间限制，那么 bash 工具的执行就会被卡住，<strong>有了 blockMs 的话，就可以将命令的执行挂载到后台去</strong></p>
<p>同时我们也可以发现，有时候会在终端调用读取命令，有可能会读取很大的文件，所以对于 Bash 工具的输出是有限制的，当超过一个阈值（目前设计的是 4000），<strong>就将 bash 工具的结果先写入到临时文件中去，返回结果的时候将临时文件路径返回</strong>，模型在需要的时候会自动调用相应工具读取的</p>
<p>那么我们回到具体的两个关键设计点中：<strong>挂载后台和写入临时文件</strong></p>
<ol>
<li>
<p>写入临时文件可以防止内存被撑爆，同时也可以提高上下文的效率，只让必要的信息进入上下文，只在必要的时候读取</p>
</li>
<li>
<p>挂载后台，这一步最关键的是挂载之后，模型如何获取后台命令的执行情况，当然模型可以主动根据文件路径进行读取，但是这样的效率不高，所以我们设计了<strong>增量读取工具</strong><code>bash_output</code><strong>和事件订阅推送</strong></p>
</li>
</ol>
<p>增量读取工具 bash_output，它可以高效的返回增量信息，而不是全部的文件内容，同时也可以返回后台执行的任务状态，提供给模型判断任务情况，比一般的读取工具返回的结果更有意义一些</p>
<p>事件订阅推送的实现是：每一次将 bash 工具的输出写入到临时文件的时候，会触发一个规则判断（正则判断或者状态判断），当条件符合的时候，那么该函数就会将 bash 工具输出的这一部分信息推送到下一轮的 turn 中的上下文。</p>
<blockquote>
<p>[!NOTE]
我们的设计中，没有采用模型定时轮询任务状态，这样效率太低啦，应该是载触发一定条件的时候，任务侧主动推送。</p>
</blockquote>
<h2>二、沙盒与权限设计</h2>
<p>沙盒和权限设计管的侧重点是不同的</p>
<ul>
<li>
<p>权限设计管的是：这个命令需不需要用户审核一下，这个命令是否可以执行</p>
</li>
<li>
<p>沙盒设计管的是：这个命令执行之后，能影响的程度有多大，保证命令执行兜底的安全</p>
</li>
</ul>
<p>在 mac 系统中，可以使用内置的沙盒机制来实现，用一份命令启动的 Profile 语法文件规则就可以，</p>
<blockquote>
<p>[!TIP]
具体的使用也很简单，在执行命令的时候/<code>usr/bin/sandbox-exec -f profile.sb</code>来启动进程，约束是由内核来完成的，并且会自动继承到整个进程树中去</p>
</blockquote>
<p>沙盒中的这份 Profile 语法文件，是参考了一下 anthropic 开源的一个轻量级沙盒库中的核心规则，不太想直接使用开源库来实现，因为我希望自己这份沙盒配置文件可以足够的透明</p>
<p>那么完整的设计结合思路如下：一共是三层设计，先进行常规的危险命令的执行拦截，然后先在沙盒中执行命令，如果沙盒中因为权限不足执行失败，那么在进入到真实环境中执行命令，但是在执行之前都要 ask 一下用户</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/agentbash2.Dw9yIVjo_ZeYXwi.webp" alt="三层沙盒与权限设计" /></p>
<p>在沙盒因为权限执行失败的时候，要记得转换一下失败输出的信号，不然 Agent 拿到 EPERM 这种错误信息，会认为是执行命令书写错误导致的，而不是因为沙盒权限不足导致的，<strong>所以我们要添加一段提示信息"这可能是沙盒拦的，不是命令错了"</strong></p>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[揭秘 AI 代理的评估：多种 Agent 的评估方法]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/agent-eval-methods/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/agent-eval-methods/</guid>
            <pubDate>Sun, 12 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[从编码 Agent、对话 Agent、研究 Agent 到计算机使用 Agent，梳理不同类型 Agent 的评估要点与 pass@k / pass^k 指标。]]></description>
            <content:encoded><![CDATA[<h2>前言</h2>
<blockquote>
<p>[!NOTE]
在上一篇《Agent 的评估》中，概述都是宏观方向的评估概念，没有具体的使用例子，本篇文章具体到几种类型的 Agent 评估方法是什么、评估的角度是什么</p>
</blockquote>
<p>分析参考来源：</p>
<ul>
<li>文章链接：https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents</li>
<li>𝜏-Bench：https://arxiv.org/abs/2406.12045</li>
<li>τ2-Bench：https://arxiv.org/abs/2506.07982</li>
<li>BrowseComp：https://arxiv.org/abs/2504.12516</li>
</ul>
<h2>一、评估编码 Agent 的方法</h2>
<p>编码 Agent 的主要任务：编写、测试和调试代码，像人类开发者一样在代码库中检索浏览，所以编码 Agent 是依赖于明确指定的任务，根据这一点，我们可以知道：<strong>确定性评分器非常适合编码 Agent</strong></p>
<p>🌴 <strong>第一方面的评估要点是：代码能否运行、测试是否通过</strong></p>
<p>这里介绍两种编程基准</p>
<ol>
<li>SWE-bench Verified</li>
<li>Terminal-Bench</li>
</ol>
<blockquote>
<p>[!NOTE]
1、Terminal-Bench 这个的理解就是：其不是修复单一的编译错误，而是完成整个编译过程，这个就是端到端的测试，从开始到结束，例如：部署 Web 应用、从零搭建 Mysql 数据库
2、SWE-bench Verified 是一种"单元测试"，常规的使用方法：</p>
<ul>
<li>给 Agent 一个真实的问题</li>
<li>Agent 开始编写修复代码</li>
<li>运行测试套件，保证 Agent 编写的修复代码可以通过测试</li>
</ul>
</blockquote>
<p>🌴 <strong>第二方面的评估要点是：Agent 的工作过程是否合理高效</strong></p>
<p>当你有了测试案例集｜测试函数来验证编码 Agent 执行的任务的结果的时候，<strong>评估编码 Agent 的工作过程也是很有用</strong>的，不仅要单一的评估测试结果是否通过，也要观察评估完成任务的过程是否合理以及优雅</p>
<p>这个时候有两种额外的评估方法</p>
<ol>
<li><strong>基于启发式规则的代码质量评估</strong>：也就是用代码规则来检查代码质量，而不是只看测试结果
<ul>
<li>代码的复杂度</li>
<li>代码的重复率</li>
<li>命名的规范</li>
<li>安全漏洞</li>
<li>性能问题</li>
<li>代码的可读性</li>
</ul>
</li>
<li><strong>基于模型的行为评估</strong>：用 大模型去评估 Agent 的执行任务的中间过程，也就是行为</li>
</ol>
<p>例如：任务 A - 查询数据库中的用户信息</p>
<p>AgentA 的做法：直接查询所有用户的信息，在内存中进行过滤
AgentB 的做法：用 where 语句条件查询用户信息，最后返回需要的数据</p>
<p>在这种情况下，虽然 A 与 B 都完成啦任务，但是 AgentB 其实是做得更好的，更符合规范的</p>
<p>🌟 <strong>结论：编码 Agent 的评估，要评估两个主要方向，编码 Agent 的执行结果和执行过程</strong></p>
<p>案例：这是一个完整的案例，实际使用的时候可以动态调整，不必全部都有</p>
<pre><code>task:
  id: "fix-auth-bypass_1"
  desc: "修复当密码字段为空时的认证绕过漏洞..."
  graders:
    - type: deterministic_tests
      required:
        - test_empty_pw_rejected.js
        - test_null_pw_rejected.js
    - type: llm_rubric
      rubric: prompts/code_quality.md
    - type: static_analysis
      commands:
        - eslint
        - tsc
    - type: state_check
      expect:
        security_logs:
          event_type: "auth_blocked"
    - type: tool_calls
      required:
        - tool: read_file
          params:
            path: "src/auth/*"
        - tool: edit_file
        - tool: run_tests
  tracked_metrics:
    - type: transcript
      metrics:
        - n_turns
        - n_toolcalls
        - n_total_tokens
    - type: latency
      metrics:
        - time_to_first_token
        - output_tokens_per_sec
        - time_to_last_token
</code></pre>
<h2>二、评估对话 Agent 的方法</h2>
<p>对话代理在与用户互动时，涉及支持、销售或辅导等领域。与传统聊天机器人不同，它们会保持状态、使用工具，并在对话中途采取行动。</p>
<blockquote>
<p>[!IMPORTANT]
虽然编程和研究代理也可能涉及与用户的多次互动，<strong>但对话代理呈现出一个独特的挑战：互动本身的质量也是你评估的一部分</strong>。</p>
</blockquote>
<p>对话代理的有效评估通常依赖于<strong>可验证的最终状态结果和能够捕捉任务完成与互动质量</strong>的评分标准。</p>
<p>与其他大多数评估不同，它们通常需要第二个 LLM 来模拟用户。我们使用这种方法在我们的对齐审计代理中，通过长时间的对抗性对话来测试模型。</p>
<p>🌴 第一方面的评估要点：<strong>可验证的最终状态</strong>，也就是对话 Agent 最终要完成的任务，例如：客服退款、修改收货地址、生成报价单等</p>
<p>🌴 第二方面的评估要点：相比其他类型 Agent 的独特的挑战：<strong>互动本身的质量也是你评估的一部分</strong></p>
<p>例如：场景 - 客服退款</p>
<p>Agent A:</p>
<p>用户："我要退款"</p>
<p>Agent："订单号？"</p>
<p>用户："12345"</p>
<p>Agent："已退款"</p>
<blockquote>
<p>任务完成 但态度生硬</p>
</blockquote>
<p>Agent B:</p>
<p>用户："我要退款"</p>
<p>Agent："很抱歉给您带来不便。请问是哪个订单呢？"</p>
<p>用户："12345"</p>
<p>Agent："我查到了您的订单，符合退款条件。我现在为您处理，预计 3-5 个工作日到账。还有其他需要帮助的吗？"</p>
<blockquote>
<p>任务完成 交互体验好</p>
</blockquote>
<p><strong>结论：所以对话 Agent 的评估标准是：最终状态验证 + 交互质量的评估</strong></p>
<p>一个对话 Agent 是否有效的标准可以是多维度的：</p>
<ol>
<li>用户的问题和诉求是否解决（状态检查）、</li>
<li>是否在 10 轮对话中完成（文本上下文的约束）、</li>
<li>语气是否恰当（LLM 来评估）</li>
</ol>
<p>有两个多维度的测试基准，其模拟了零售支持和航空预订等领域的多轮交互，其中使用了一个 LLM 扮演用户角色，这两个测试基准：<strong>𝜏-Bench 及其后续版本τ2-Bench</strong></p>
<blockquote>
<p>[!TIP]
在开发类似场景和领域的客服对话 Agent，可以使用这两个测试基准来评估自己开发的 Agent 是否有效</p>
</blockquote>
<p>一个测试评估案例，对话 Agent 处理沮丧用户的退款</p>
<pre><code>graders:
  - type: llm_rubric
    rubric: prompts/support_quality.md
    assertions:
      - "Agent 对客户的沮丧表现出同理心"
      - "解决方案被清晰地解释"
      - "Agent 的回复基于 fetch_policy 工具的结果"
  - type: state_check
    expect:
      tickets:
        status: resolved
      refunds:
        status: processed
  - type: tool_calls
    required:
      - tool: verify_identity
      - tool: process_refund
        params:
          amount: "&lt;=100"
      - tool: send_confirmation
  - type: transcript
    max_turns: 10
tracked_metrics:
  - type: transcript
    metrics:
      - n_turns
      - n_toolcalls
      - n_total_tokens
  - type: latency
    metrics:
      - time_to_first_token
      - output_tokens_per_sec
      - time_to_last_token
</code></pre>
<h2>三、评估研究 Agent 的方法</h2>
<p>研究 Agent 的主要任务是：研究代理收集、综合和分析信息，然后产生输出，如答案或报告</p>
<p>该 Agent 的评估无法类似于编码 Agent 单元测试那么确定，<strong>研究 Agent 的输出质量的评估只能是相对任务进行判断，主要是：</strong></p>
<ul>
<li>全面的搜索和研究</li>
<li>有良好的且正确的来源</li>
</ul>
<p>并且不同领域的任务，评估的标准也是不一样的，例如：市场研究和技术调研是需要不同的标准</p>
<p>研究 Agent 评估面临独特挑战：<strong>专家可能对综合是否全面存在分歧，真实情况会随着参考内容不断变化，而更长、更开放式的输出会为错误创造更多空间</strong></p>
<p>比较有名的测试基准是：<strong>BrowseComp</strong></p>
<p>这样的基准测试 AI 代理能否在开放网络中找到针子——<strong>这些问题设计得容易验证但难以解决</strong>。</p>
<blockquote>
<p>[!NOTE]
BrowseComp 是 OpenAI 发布的一个 AI 代理浏览能力基准测试，专门评估 AI 能否在开放网络中找到"难以发现"的信息。但是答案非常好验证，一般都是一个词或短语，方便开发者进行评估</p>
</blockquote>
<p>所以构建研究 Agent 的评估的一般方式是：组合多种评分器类型</p>
<ol>
<li>基础性检查：检查验证每一个声明都有来源支持吗？</li>
<li>覆盖性检查：来源里面的关键信息都包含了吗？都使用了吗？</li>
<li>来源质量检查：引用的资料是否权威，不能因为在网络搜索排名第一就使用它</li>
</ol>
<p>我们使用一个例子来说明这三种检查的主要方向：</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-56.DXEyH7-5_1zqj1Q.webp" alt="研究类型的 Agent 的评估" /></p>
<h2>四、评估计算机使用 Agent 的方法</h2>
<p>计算机使用 Agent 通过与人类相同的界面与软件交互</p>
<ul>
<li>屏幕截图</li>
<li>鼠标点击</li>
<li>键盘输入和滚动</li>
</ul>
<p>而不是通过 API 或代码执行和软件交互，<strong>计算机 Agent 可以使用任何带有图像用户界面的程序</strong></p>
<p>那么评估这种类型的 Agent，不仅仅是评估界面是否出现，还要评估软件后面的逻辑是否正确执行，例如：</p>
<ol>
<li>WebArena 测试基于浏览器的任务，<strong>使用 URL 和页面状态检查来验证代理是否正确导航</strong>，同时对修改数据的任务进行后端状态验证（确认订单确实已下单，而不仅仅是确认页面出现了）</li>
<li>OSWorld 将此扩展到完整的操作系统控制，评估脚本在任务完成后检查各种产物：文件系统状态、应用程序配置、数据库内容和 UI 元素属性</li>
</ol>
<p>🌟这一个设计思路非常重要，引用官方原文：</p>
<blockquote>
<p>浏览器使用代理需要在 token 效率和延迟之间取得平衡。基于 DOM 的交互执行速度快但消耗大量 token，而基于屏幕截图的交互速度较慢但 token 效率更高。</p>
</blockquote>
<p>如果要开发一个浏览器的 Agent，那么在执行的行为中可以考虑这个方向：<strong>操作 DOM 还是网页截图</strong></p>
<ol>
<li>如果网页的文本较多，那么直接读取 DOM 元素会更加的高效，并且信息密度很大，无用的网页标签会大大减少</li>
<li>如果网页的 DOM 很多，文本信息非常的分散，典型的就是电商网站，商品推荐任务，可以考虑截图，截图会更高效和清晰</li>
</ol>
<h2>五、总结</h2>
<p>无论智能体类型如何，智能体行为在每次运行中都会变化，这使得评估结果比最初看起来更难解释。</p>
<p>每个任务都有其自身的成功率可能在某个任务上达到 90%，在另一个任务上只有 50% 一个在某个评估运行中通过的任务，在下一个运行中可能会失败。</p>
<p>有时，我们想要测量的是智能体在某个任务上成功的频率（即试验的比例）</p>
<p>有两个指标有助于捕获这种细微的差异：</p>
<p><strong>1、pass@k 衡量代理在 k 次尝试中至少获得一个正确解决方案的可能性。</strong></p>
<p>🌟 随着 k 的增加，pass@k 分数会上升——更多的"射门机会"意味着至少 1 次成功的几率更高。</p>
<p>50% 的 pass@1 分数意味着模型在评估中第一次尝试就成功完成了半数任务。在编程中，我们通常最关心代理第一次就找到解决方案——pass@1。在其他情况下，只要有一个解决方案有效，提出许多解决方案也是可以的。</p>
<p>例如：pass@3 的案例解释</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-55.Cqq3GajB_ZyyRrW.webp" alt="pass@k 案例" /></p>
<p>总共有 5 个任务，在 3 次机会里面至少成功一次的有 3 个任务，所以 pass@3 = 60%，可以注意到在任务三中，Agent 在第四次机会执行成功了，但是不作为 pass@3 的判断标准里面了，所以无效</p>
<p><strong>2、pass^k 衡量所有 k 次试验成功的概率。</strong></p>
<p>🌟 随着 k 的增加，pass^k 会下降，因为要求在更多试验中保持一致性是一个更难达到的标准。</p>
<p>如果你的代理每次试验的成功率为 75%，而你运行了 3 次试验，那么全部 3 次试验成功的概率是 (0.75)³ ≈ 42%。这个指标对于面向用户的代理尤其重要，因为用户期望每次都能获得可靠的行为</p>
<p>这两个指标可以作为捕获 Agent 的差异，</p>
<ol>
<li>一个表示可用性，pass@k，说明 Agent 的潜力是多少，给足够的机会，它可以做些什么，它的边界在哪里</li>
<li>一个表示稳定性，pass^k 说明 Agent 有多可靠，衡量这个 Agent 在任务中的靠谱性</li>
</ol>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-58.CtAUrezw_Z20sA1n.webp" alt="pass@k 与 pass^k 分化示意图" /></p>
<blockquote>
<p>随着试验次数的增加，pass@k 和 pass^k 出现分化。在 k=1 时，它们是相同的（都等于每次试验的成功率）。到 k=10 时，它们呈现出截然相反的情况：pass@k 接近 100%，而 pass^k 降至 0%。</p>
</blockquote>
<p><strong>两种指标都很有用，使用哪种取决于产品需求：对于工具，一个成功就很重要，使用 pass@k；对于代理，一致性是关键，使用 pass^k。</strong></p>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[工具调度与权限模块的开发]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/tool-dispatch-permission/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/tool-dispatch-permission/</guid>
            <pubDate>Sat, 11 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[设计 Agent 的工具执行调度——7 种状态机、4 层权限模式、配置文件规则系统与 allowList 机制，参考 ClaudeCode / Gemini-cli / OpenCode / Kode。]]></description>
            <content:encoded><![CDATA[<p>工具权限模式的开发重点就是通过设计几种 Agent 模式以此来判断执行工具的时候，这个工具是否需要用户审核批准执行，但是以此会延伸出来<strong>工具调度的实现和 allowList 的机制</strong></p>
<p>所以工具权限模块的完整的开发方式是：</p>
<ol>
<li>工具的权限验证方法和终端的权限验证面板</li>
<li>工具执行的调度</li>
<li>allowList 机制</li>
</ol>
<p>参考的分析资料：</p>
<ul>
<li>Gemini-cli：<a href="https://github.com/google-gemini/gemini-cli">https://github.com/google-gemini/gemini-cli</a></li>
<li>OpenCode：<a href="https://github.com/anomalyco/opencode">https://github.com/anomalyco/opencode</a></li>
<li>Kode：<a href="https://github.com/shareAI-lab/Kode-cli">https://github.com/shareAI-lab/Kode-cli</a></li>
<li>ClaudeCode</li>
</ul>
<h2>一、工具调度流程的设计</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/XiqibKuhCo6vHfxflwQcGdq9n73.Bo3_SPQ-_1TKeRN.webp" alt="工具调度流程设计" /></p>
<p>工具的调度流程设计中，工具的执行状态有以下几种：</p>
<ol>
<li>validating（验证中）：验证工具的参数等前置状态是否正确</li>
<li>awaiting_approval（等待确认）：需要用户批准，正在等待用户批准</li>
<li>scheduled（已调度）：工具准备开始执行的，等待批量执行</li>
<li>executing（已执行）：正在执行工具</li>
<li>success（执行成功）：工具执行成功</li>
<li>error（执行失败）：工具执行失败</li>
<li>cancelled（取消执行）：用户取消执行工具或者进程中断</li>
</ol>
<h2>二、工具权限验证方法的设计</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/Qz9jbUbvXo941oxk3RNcCPLknve.GPiQZRz2_cwO1u.webp" alt="四种权限模式" /></p>
<ol>
<li>计划模式：是完全不能使用编辑和执行命令的工具</li>
<li>默认模式：是所有的工具都可以使用，但是需要批准</li>
<li>编辑模式：所有的工具都可以使用，编辑类工具自动批准，执行命令工具依旧要批准</li>
<li>自动模式：所有的工具都可以使用，所有的工具都自动批准</li>
</ol>
<p>前端可以进行模式的切换，当 Agent 需要执行一个工具的时候，在工具执行调度模块的地方，会执行工具的验证函数，<strong>每一个工具几乎都有一个验证函数</strong>，验证函数的实现核心是：</p>
<ol>
<li>获取前端传递过来的模式</li>
<li>根据模式进行判断该工具是否需要审核批准</li>
<li>对于命令执行工具的执行，在命令执行工具的验证函数中会进行 allowList 机制判断</li>
</ol>
<p>当验证函数返回需要审核批准的时候，就开始进入第二阶段，在第二阶段中就是获取用户的选择，目前有三种模式：</p>
<ul>
<li>执行一次：同意本次工具的执行</li>
<li>本次会话允许：在这个会话中，该工具的执行全部都自动批准后续</li>
<li>取消：不允许执行该工具</li>
</ul>
<p>那么本次会话允许的话，对于两类工具的表现是不同的：<strong>命令执行工具和编辑类工具</strong></p>
<ol>
<li>命令执行工具：使用 <code>allowList</code> 机制保留命令执行的"前缀"，下一次判断就进行前缀的验证</li>
<li>编辑类工具：编辑类工具会切换模式，将模式切换为"编辑模式"</li>
</ol>
<h2>三、工具权限配置文件的设计</h2>
<p>上面那种工具自身提供验证函数的设计，可以发现模式和用户是被动的，而开发者在设计函数的时候是主动的，</p>
<p>也就是说用户无法修改某一个工具的验证行为，例如：我就想 xxx 工具一直通过，不需要验证，</p>
<p>用户只能被动的通过切换模式整体设置，这样在权限验证是不够安全的，用户对于 Agent 的工具调用控制程度也很低</p>
<p>为了让权限模块更完善，我们切换设计角度，<strong>让工具成为被动挑选的，用户成为主动</strong>，也就是说<strong>用户可以通过修改配置文件来达到控制工具验证行为的效果</strong></p>
<pre><code>// agent 的配置文件
{
    "permissions": {
      "allow": ["Read", "Bash(git *)"],
      "deny":  ["Bash(rm -rf*)"],
      "ask":   ["Bash(npm publish*)"]
    }
  }
</code></pre>
<p>对于配置文件的设计，ClaudeCode 最细节，有 8 个配置数据的来源：userSettings、projectSettings、localSettings、flagSettings、policySettings、cliArg、command、session。这 8 个文件关于权限规则的部分是叠加的，不会覆盖</p>
<p>那么完整的权限验证系统：</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/IZFgblnV0otIZgx7TJEc2f6cnSe.CjOsii0q_1XVtRN.webp" alt="完整的权限验证系统" /></p>
<ol>
<li>在权限验证方法中有两层大验证，是先后优先级的关系，<strong>规则判断先，工具验证函数后</strong>，如果规则判断通过就直接执行，不继续验证，只有没通过的时候才继续验证工具函数</li>
<li>deny（拒绝执行）：这个规则调用位置有点特殊，<strong>分为两次调用</strong>，第一次是在工具注册的时候静态执行，如果匹配到拒绝执行的工具，那么都不注册，模型看都看不到，第二次是在工具执行阶段中的权限验证环节，两次验证的原因是，如果存在工具注册缓存的话，第二次就非常有必要啦，因为有一些工具会动态被加载进来，或者规则修改啦</li>
<li>allow（允许执行）：这个规则的执行顺序有点特别，**是在工具验证函数之后执行的，**因为当用户配置了 allow 数组，a 工具允许执行，但是 a 工具的自身验证函数输出的是普通的 ask，或者命令行工具的前缀验证输出的是"我木有意见，交给上面决定"的 passthrough，那么 allow 就可以静默它们这些状态直接放行，刚好发挥了用户配置的作用（可能不是那么大吧）</li>
</ol>
<p>🎃还有一点小细节的设计：命令行的工具验证和获取方式和其他的工具是不一样的，其会有一些命令前缀的验证过程，匹配的时候也是要单独注意的</p>
<h2>四、终端显示执行的效果和方式</h2>
<p>工具的执行状态有利于 cli 终端进行状态的显示，Agent 端进行事件通知采用"发布 - 订阅"的方式，让 cli 终端可以得到工具的执行状态的推送，那么 cli 终端就可以进行自定义的状态显示</p>
<ul>
<li>validating 的时候就显示工具等待中</li>
<li>awaiting_approval 的时候就显示审核面板，让用户选择执行的方式</li>
<li>executing 的时候就显示工具执行中的状态</li>
<li>success、error、cancelled 的时候就当作工具的执行结果显示在 cli 终端</li>
</ul>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/Gk1ObQjtHoG1zAxzUD0czw0Onkc.BUqWVMF9_Z13w46f.webp" alt="终端审核面板示例" /></p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/Juw2bmArkogn6txHuPIcbI0dnye.Bx0vpCqq_Z1OVLfr.webp" alt="工具执行中状态显示" /></p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/DILyb8o5wo1Df2xkGckcTAUpnqb.Ca6vvSjU_1k3KP7.webp" alt="工具执行结果显示" /></p>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[多智能体的协作方式：Agent Team 和 Agent Room]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/agent-team-room/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/agent-team-room/</guid>
            <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[探索多智能体协作的两种核心模式——Agent Team（临时团队攻坚）和 Agent Room（平等讨论），以及任务调度与成员设计。]]></description>
            <content:encoded><![CDATA[<p>最近在实践多智能体的设计方式，也准备考虑给 ActSpace 添加多智能体的功能，主要是多智能体如果设计得当的话，是可以在提高效果的同时节省成本的，这对于很多做应用的团队来说，成本的控制是可以带来极好的用户体验的，我觉得这也是 Harness Engineering 的概念之一</p>
<p>在不断的探索的过程中，我有一种新的感觉，多智能体的设计，尤其是协作方面非常的有意思，而且很有挑战性，我也感觉这个也是目前很多成熟 Agent 在突破的主要方向之一，从 ClaudeCode 推出来的 Agent Team 和 Dynamic Workflows 可以感受到一些</p>
<p>下面记录了我在多智能体协作上的部分探索与思考，谨小认知，仅供参考</p>
<p>调研资料：</p>
<ul>
<li>《Is Having Agents in the Room Meant to Be Chaotic?》：https://raft.build/resources/blog/is-having-agents-in-the-room-meant-to-be-chaotic/</li>
<li>《multica 开源项目》：https://github.com/multica-ai/multica</li>
</ul>
<p>::github{repo="multica-ai/multica"}</p>
<ul>
<li>《Model and effort in Claude Code: knowing more vs. trying harder》：https://x.com/ClaudeDevs/status/2074900291062034618</li>
</ul>
<h2>一、Agent Team 的设计思路</h2>
<p>Agent Team 是多智能体的协作方式之一，类似于一个组"临时团队"一起攻坚一个复杂的任务，里面会有负责人的角色（Lead）、同时也会存在团队的成员（Teammate），交流关系不仅仅是负责人和成员之间的交流，成员和成员之间同样可以交流</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/AR-AT-1.D3PTLp7__Z2n1CpG.webp" alt="Agent-Team" /></p>
<p><strong>一个 Agent Team 最核心的四个组件：团队负责人、任务清单、团队成员、消息收件箱</strong></p>
<p>核心的运行步骤如下：</p>
<ol>
<li>Team Lead 根据任务的复杂性生成任务清单，同时开始创建团队的基本信息</li>
<li>Lead 可以直接分配任务给团队成员做，同时在空闲的时候团队成员也可以主动领取任务</li>
<li>Lead 和 Teammate 的交流，不是直接交流的，是通过 inbox(消息收件箱）发送消息，类似于人类协作中的发送邮件的操作</li>
</ol>
<p>Team Lead 创建任务和团队基本信息的时候，是依靠两个工具：taskCreate 和 TeamCreate，这两个工具会创建两个关键的文件夹的，我们假设团队名叫做 jink-team，那么创建的文件夹结构如下：</p>
<pre><code>~/.claude/teams/jink-team/
~/.claude/tasks/jink-team/
</code></pre>
<p>🌵 那么任务清单创建好之后，如何分配任务呢？</p>
<p>Agent Team 是通过<strong>Lead 分配和团队成员主动领取</strong>这两个方式，Lead 分配任务使用的是 TaskUpdate 方法，团队成员主动领取任务使用的是 TaskList 先获取任务的状态，然后根据状态来领取任务，这是程序中循环调度器实现的，每 500ms 循环执行一次。</p>
<p>最核心的地方来啦，团队的协作交流，Agent Team 中的交流信号分为两种：信息和指令，是通过消息收件箱传递的</p>
<ol>
<li>信息就是一些内容，一些任务的描述和输入，就和正常的用户输入是一样的</li>
<li>指令是一些硬信号，例如：权限的审核通知，团队成员会将执行权限审批先发送给负责人的收件箱，负责人在读取之后，将该指令传递给前端用户，用户审核之后，状态在一步一步回流，还有团队成员的进程关闭等</li>
</ol>
<p>消息收件箱也很简单有效，就是一个 json 文件，里面的消息会有一个是否读取的状态，每一个 Agent 执行的之后，都会有循环调度去反复去读取这个文件，如果有新消息，会在 Agent 的下一轮将消息注入到上下文中执行</p>
<p>所以我们可以发现，Agent Team 的消息传递只是通过简单的文件 + 调度器的方式，非常有效，不过在实现这种传递方式时，最关键的是要注意文件锁的设计</p>
<p>例如：任务清单很可能会出现两个 Agent 同时读取同一个任务，这个时候状态会变得不稳定，所以我们要设置文件锁，同一个时刻，只有由一个 Agent 读取并且执行修改。</p>
<blockquote>
<p>[!NOTE]
当某一个 Agent 抢到文件锁的时候，那么该文件的所有权就是交给这个 Agent 来控制啦，其他的 Agent 只能在旁边先等待</p>
</blockquote>
<p>ClaudeCode 团队的实现非常的有灵性，简单的使用一个 <code>mkdir</code> 方法就实现啦，<code>mkdir</code> 在文件系统上是原子的：同一时刻只有一个人能建成这个目录</p>
<blockquote>
<p>我这里简单介绍一下：当 Agent 读取 <code>3.json</code>任务时，会在同级目录下创建一个<code>3.json.lock/</code>文件夹，那么其他的 Agent 使用 mkdir 创建这个文件的时候，会发现文件已经存在，就会进行等待状态，当 Agent 操作完成<code>3.json</code>这个任务之后，就会主动删除这个<code>3.json.lock/</code> 文件夹</p>
</blockquote>
<pre><code>tasks/jink-team/
  3.json          ← 真正的任务内容
  3.json.lock/    ← 「有人正在改 3.json」（锁的物理形态）
</code></pre>
<h2>二、Agent Room 的设计思路</h2>
<p>Agent Room 同样也是多智能体的协作方式之一，这个里面没有"Lead"，是一次平等的交流，互相讨论，发表意见，在一个房间里面多智能体进行思想碰撞</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/AR-AT-2.DprrLgjX_ZzN33U.webp" alt="Agent-Room" /></p>
<p>对于 Agent Room 中的理解，核心在上下文的主动与被动，如果从聊天室出发，将 Agent 拉入，那么聊天室的每一次消息都会灌入 Agent 的上下文中，Agent 是被动接受这些信息的，从上下文的管理角度来说，这种方式的处理非常糟糕</p>
<p>因为每一条注入上下文的消息，模型都会关注的，如果无关的信息过多，冲突的信息存在，那么这些都会干扰 Agent 去做决定，我们从 Agent 的角度去理解就很直白啦</p>
<p>那么怎么理解，或者说怎么做是不错的协作方式呢？对于 Agent Room 来说，核心是两点概念：<strong>收件箱和草稿板</strong></p>
<ul>
<li>收件箱：聊天室的每一次消息都会放入到 Agent 的聊天室收件箱中，但是要推入什么信息到 Agent 上下文中，完全由 Agent 自己来决定，它可以选择性的拉去收件箱中的消息</li>
<li>草稿板：Agent 每一次真正输入到聊天室之前，先判断一下聊天室或者收件箱的消息列表是否更新啦，如果没有更新，那么直接输出，如果更新啦，那么消息被暂存并且添加一些附加信息再次注入回上下文执行，这次 Agent 有四种选择修改、原样发送、放弃、强制发送</li>
</ul>
<blockquote>
<p>这里的概念 Raft 博客说的特别好，详细完整的大家可以去看看原文：https://raft.build/resources/blog/is-having-agents-in-the-room-meant-to-be-chaotic/</p>
</blockquote>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/AR-AT-3.V-M85TBU_Qe97s.webp" alt="Agent-Room-2" /></p>
<p>我这里还有一种思路，大家可以参考借鉴的，没有收件箱和草稿板，聊天室的消息直接被推入到 Agent 的上下文中，但是多了一个概念：叫做思维精灵（子 Agent）</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/AR-AT-4.TzVBtXsw_1zqf2G.webp" alt="Agent-Room-3" /></p>
<p>设想是这样的：Agent 不进行任务的执行，只负责分发任务给思维精灵执行，最终等待子智能体结果进行综合分析回复，在回复之前，会调用工具"抢占发言令牌"，相当于班级中的"举手发言"啦，举手成功之后，就可以将最新的消息一并同工具返回之后，那么聊天室这段时间都属于该 Agent，那么其就进行最终的回复</p>
<p>🎃 <strong>当然这个只是一种猜想，我还没有具体实践过，感兴趣的朋友可以自行尝试梳理！！！</strong></p>
<h2>三、Agent Task 的设计思路</h2>
<p>Task 是 Agent 执行的最小单元吧，所以这里我也想要一起简单的梳理一下，一个 Task 只能被一个 Agent 执行，但是一个 Agent 可以同时执行多个 Task</p>
<p>产生 Task 的方式我们也可以结合上面两种设计来看</p>
<ol>
<li>Agent Team 产生的 Task，更多像是一个复杂任务被拆分为一个一个的小任务，可以定义为一个临时任务</li>
<li>Agent Room 产生的 Task，偏向于用户手动指定某一个成员执行一个任务，例如：执行 bug 修复任务或者执行某一个领域的检索任务，可以定义为一个完整的任务</li>
<li>用户或者 Agent Room 是可以产生一种有时间属性的任务，是定时任务，到时间就执行的任务</li>
</ol>
<p>那么对于设计多智能体项目时，是可以有一个 Task 模块的，里面可以按照昨天状态显示当前执行的各种任务，也可以按照 Agent 显示目前每一个 Agent 执行的任务是什么</p>
<h2>四、Agent Member 的设计思路</h2>
<p>在多智能体的协调中，Member 这个角色很重要，Agent Team 中就需要真正执行的 Member，Agent Room 中也是如此</p>
<p>所以 Member 是需要一个完整的定义的，它不是临时的，是有身份，有头像，有名字，有设定，有工具，有范围，有记忆等等，这种 Member 在 Agent Room 中使用会非常直接方便，就相当于 Agent Room 中的成员，一个 Member 可以进入多个房间，Room 会话互相不干扰</p>
<p>但是在 Agent Team 中我们要是从 Member 去理解的话，那么每一次 Team 总是那么几个成员，非常的固定，没有发挥组建临时团队的意义，所以这里有一种设计可以考虑，Member 有"分身"，Agent Team 中的团队成员本质是 Member 的分身，有一些核心的东西不变，但是一些其他的可以变动，每一个 Team 中的相同的 Member，但是不同的"分身"</p>
<blockquote>
<p>在 Agent Team 中，Member 的定位设计不是最重要的，重要的是可以为相应的复杂任务拉起正确的团队成员</p>
</blockquote>
<p>这样在多智能体的设计中，Member 就在 Team 和 Room 中统一起来了，就可以统一维护和设计啦</p>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[Bash 工具实现和安全权限设计细节]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/bash-tool-impl/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/bash-tool-impl/</guid>
            <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Agent Bash 工具的完整实现——工具定义、Generator 模式执行、返回值设计，以及 24 条静态安全检查规则与三层权限验证体系。]]></description>
            <content:encoded><![CDATA[<h2>一、Bash 工具的实现</h2>
<p>Bash 工具是 Agent 很关键的基础工具，skill 规范中的脚本执行会需要它，随着应用逐渐 cli 化，未来的 Agent 调用外部应用也需要它，到时候只需要执行 cli 命令就可以。</p>
<p>同时有 bash 工具之后，很多操作都可以实现，一定程度上可以简化 Agent 工具列表，<strong>但是还是建议保持工具最小权利原则</strong></p>
<blockquote>
<p>[!TIP]
任务优先使用专用工具完成，例如：读取就使用 Read 工具、编辑就使用 Edit 工具等</p>
</blockquote>
<h3>1.1、Bash 工具的定义</h3>
<pre><code>export const BashTool = buildTool({
  name: BASH_TOOL_NAME,
  searchHint: 'execute shell commands',
  description: getSimplePrompt(),
  inputSchema: z.strictObject({
    command: z.string().describe('要执行的命令'),
    timeout: z.number().optional().describe('可选的超时时间，单位为毫秒'),
    description: z.string().optional().describe('用主动语态简明扼要地描述该命令的作用'),
    //全局禁用后台任务时隐藏
    run_in_background: z.boolean().optional().describe('设置为 true 可在后台运行此命令'), 
    // 危险操作
    dangerouslyDisableSandbox: z.boolean().optional().describe('设置为 true 将强制跳过沙箱模式执行命令'), 
    // 始终对模型隐藏
    _simulatedSedEdit: z.object({ filePath: z.string(), newContent: z.string() }).optional().describe('内部字段：预览阶段预计算的 sed 编辑结果'), 
  }),
  // ... 其他方法如 call、checkPermissions 等
})
</code></pre>
<p>工具的参数：</p>
<ul>
<li>command：要执行的 bash 命令字符串</li>
<li>timeout：命令执行的超时时间，防止 Agent 运行被卡死</li>
<li>description：对命令的简短描述，UI 渲染给用户看的</li>
<li>run_in_background：有一些命令运行需要很久，例如：构建和启动命令，所以将命令执行放入到后台，定时查询任务结果就可以</li>
<li>dangerouslyDisableSandbox：一种安全防护的策略</li>
<li>_simulatedSedEdit：预览阶段先计算好 sed 结果，用户批准之后直接向文件写入该结果，而不是再执行 sed 命令，将操作从"正则替换"变为"完整内容覆盖"，保证用户所见即所得</li>
</ul>
<p>工具的描述：</p>
<p>:::fold[Bash 工具完整描述（点击展开）]</p>
<pre><code>**执行给定的 bash 命令并返回其输出**
工作目录在命令之间是持久化的，但 shell 状态不持久化。shell 环境从用户的 profile（bash 或 zsh）初始化。

重要：避免使用此工具运行 find、grep、cat、head、tail、sed、awk 或 echo 命令，除非明确指示或已验证专用工具无法完成任务。相反，应使用适当的专用工具，因为这将为用户提供更好的体验。

# Instructions
- 如果命令将创建新目录或文件，首先使用此工具运行 ls 以验证父目录存在且位置正确。
- 始终对包含空格的文件路径使用双引号引用。
- 尽量通过使用绝对路径来保持当前工作目录不变，避免使用 cd。但如果用户明确要求，可以使用 cd。
- 可指定可选超时（毫秒，最多 10 分钟）。默认 2 分钟后超时。
- 可以设置 run_in_background 在后台运行命令。
- 执行多个命令时：
   - 如果命令独立且可并行，在单条消息中发起多个 Bash 调用。
   - 如果命令相互依赖必须顺序执行，用单个 Bash 调用配合 &amp;&amp; 链式执行。
   - 仅在需要顺序执行但不在乎前面命令是否失败时使用 ;。
   - 不要用换行符分隔命令（换行在引号字符串内是可以的）。
- git 命令：优先创建新提交而非修改现有提交；破坏性操作前先考虑更安全的替代方案；不要跳过 hook。
- 避免不必要的 sleep：能立即执行的命令不要 sleep；长时间运行的命令用 run_in_background；不要用 sleep 循环重试失败的命令；等待后台任务时无需轮询。
- 沙箱说明（如果启用了沙箱）：默认在沙箱中运行，限制可访问的目录和网络；临时文件要用 $TMPDIR 而非 /tmp。
- git 提交与 PR 的详细操作指南（很长的一段，包含具体的 git status / git diff / git log / gh pr create 等步骤和最佳实践）。
</code></pre>
<p>:::</p>
<h3>1.2、工具的执行函数</h3>
<p>在实现 Bash 工具的执行函数的时候，主要就是下面的三点核心思路：</p>
<ol>
<li>采用 Generator 函数来实现，可以实时输出命令执行情况</li>
<li>生产级别的命令执行函数 exec 要封装好</li>
<li>Bash 工具返回结果时，需要进行内容字符长度的判断，如果超出自定义的字符最大限制，就将完整内容写入文件，最终返回<strong>部分结果与文件路径</strong></li>
</ol>
<p>在 Bash 工具实现中，Generator 模式比 Promise 模式更优秀，Geneator 模式可以实时返回命令执行过程的中间态，但 Promise 模式在执行命令的时候需要持续等待，中间可能会有一大段空白的等待时间，这个过程没有任何输出。</p>
<p>下面这个代码是对于 Bash 工具的核心实现逻辑（简化版本），同时对比了 Generator 模式和 Promise 模式的实现区别</p>
<pre><code>import { spawn } from 'child_process';

//使用 Promise 模式
function runWithPromise(command: string, args: string[]): Promise&lt;{ stdout: string;
 code: number }&gt; {
  return new Promise((resolve) =&gt; {
    const proc = spawn(command, args);
    let stdout = '';

    // 只要子进程有输出，就拼接到 stdout 字符串里
    proc.stdout.on('data', (chunk) =&gt; {
      stdout += chunk.toString();
    });

    // 等子进程完全结束后，一次性 resolve
    proc.on('close', (code) =&gt; {
      resolve({ stdout, code: code ?? 0 });
    });
  });
}
const result = await runWithPromise('ping', ['-c', '5', 'google.com']);
console.log("==Promise==")
console.log(result.stdout); // 用户等了 5 秒，然后突然全部刷出来

//使用 Generator 模式
async function* runWithGenerator(command: string, args: string[]) {
  const proc = spawn(command, args);
  let fullOutput = '';

  // 把"下一行输出"包装成一个 Promise
  let resolveNextLine: ((value: string) =&gt; void) | null = null;
  proc.stdout.on('data', (chunk) =&gt; {
    const text = chunk.toString();
    fullOutput += text;
    if (resolveNextLine) {
      resolveNextLine(text);   // 唤醒 generator
      resolveNextLine = null;
    }
  });

  // 把"进程结束"包装成一个 Promise
  let resolveExit: ((code: number) =&gt; void) | null = null;
  proc.on('close', (code) =&gt; {
    if (resolveExit) resolveExit(code ?? 0);
  });
  const exitPromise = new Promise&lt;number&gt;((resolve) =&gt; {
    resolveExit = resolve;
  });

  // 核心循环：race 等待"新输出"或"进程结束"
  while (true) {
    const nextLinePromise = new Promise&lt;string&gt;((resolve) =&gt; {
      resolveNextLine = resolve;
    });

    // 关键：同时监听两个事件，哪个先到就处理哪个
    const winner = await Promise.race([
      nextLinePromise.then(text =&gt; ({ type: 'output' as const, text })),
      exitPromise.then(code =&gt; ({ type: 'exit' as const, code })),
    ]);

    if (winner.type === 'exit') {
      // 进程结束了，return 最终值
      return { stdout: fullOutput, code: winner.code };
    }

    // 进程还在跑，有新输出，yield 进度
    yield {
      output: winner.text,      // 这次新增的内容
      fullOutput,                // 截至目前全部内容
    };
  }
}
const gen = runWithGenerator('ping', ['-c', '5', 'google.com']);
while (true) {
    const step = await gen.next();
  
    if (step.done) {
      console.log('Exit code:', step.value.code);
      break;
    }
    console.log("==Generator==")
    console.log(step.value.output);
  
    process.stdout.write(step.value.output);
}
</code></pre>
<p>在查看 ClaudeCode 的设计思路中，<strong>如果这个 bash 工具的实现要更完善一些</strong>，可以将执行命令的 exec 方法封装</p>
<pre><code>// 调用底层 exec（file mode：stdout 直接写文件，不经过 JS data 事件）
const shellCommand = await exec(command, abortController.signal, 'bash', {
    timeout: timeoutMs,
    onProgress(lastLines, allLines, totalLines, totalBytes, isIncomplete) {
        lastProgressOutput = lastLines;
        lastTotalLines = totalLines;
        lastTotalBytes = isIncomplete ? totalBytes : 0;
        // 唤醒 race
        if (resolveProgress) {
        resolveProgress();
        resolveProgress = null;
        }
    },
});
</code></pre>
<p>exec 的主要封装的功能如下：</p>
<ol>
<li><strong>输出写入磁盘文件</strong>：程序运行内存中只有 4KB 左右的预览</li>
<li><strong>主动中断</strong>：用户能够主动触发中断执行，使用 abortSignal 实现</li>
<li><strong>超时处理</strong>：当运行超过 120 秒时，主动停止运行</li>
<li><strong>stdout 和 stderr 合并写入同一个文件</strong>：让 UI 层的显示和输出时序一致</li>
<li><strong>CWD 自动恢复</strong>：当前命令执行的目录不小心被删除啦，会自动回退到原始目录继续执行</li>
</ol>
<p>Bash 工具的执行函数中，会对于执行命令的结果返回做了一层截断处理</p>
<pre><code>const MAX_INLINE_SIZE = 128 * 1024;      // 128KB：直接返回内容
async function resolveOutput(outputFilePath: string, taskId: string) {
    const stat = await fsStat(outputFilePath);
    const totalBytes = stat.size;
  
    // 1. 小文件：直接返回
    if (totalBytes &lt;= MAX_INLINE_SIZE) {
      const content = await readFile(outputFilePath, 'utf-8');
      return {content,persistedPath: undefined};
    }
  
    // 2. 大文件：读取截断内容，复制文件，最终返回截断内容 + 完整文件路径
    const preview = await readFileRange(outputFilePath, 0, MAX_INLINE_SIZE);
    const dest = getToolResultPath(taskId, false);
    await copyFile(outputFilePath, dest);
    return {
      content:`[Large output (${totalBytes} bytes). ` +`Showing first ${MAX_INLINE_SIZE} bytes. ` +`Use FileRead for full content.]\n${preview}`,
      persistedPath: dest,
    };
  }
</code></pre>
<h3>1.3、工具的返回值</h3>
<pre><code>const data = {
    stdout: compressedStdout,                    // 核心输出（可能含 stderr 混合内容）
    stderr: stderrForShellReset,                 // 仅用于 cwd 重置提示
    interrupted: wasInterrupted,                 // 是否被中断
    isImage,                                      // 是否是图片输出
    returnCodeInterpretation: interpretationResult?.message,  // 退出码语义解释
    noOutputExpected: isSilentBashCommand(input.command),     // 成功时是否应无输出
    dangerouslyDisableSandbox: input.dangerouslyDisableSandbox,  // 是否绕过沙箱
    persistedOutputPath,                          // 大输出持久化路径
    persistedOutputSize                           // 大输出字节数
};
</code></pre>
<p>有几个核心的字段要留意：</p>
<ul>
<li>returnCodeInterpretation：对非零退出码的语义化的解释，可以让模型根据输出结果更好的推断如何进行下一步</li>
<li>perisstedOutputPath：大输出的文件路径，这个很重要，返回给模型之后，模型可以根据上下文的情况来判断是否有必要读取完整的输出，而不是一股脑的把一堆输出放入到上下文中，这样做会导致上下文使用效率非常低</li>
<li>stdout：这个就是 bash 工具执行的核心输出</li>
</ul>
<h3>1.4、<strong>工具权限验证流程</strong></h3>
<p>Bash 工具的执行范围非常大，所以它的危险性也是最高的，对于 Agent 和宿主机来说，权限验证是最复杂的</p>
<ol>
<li>命令解析</li>
<li>静态规则检查</li>
<li>权限验证</li>
<li>模型验证</li>
<li>容器验证</li>
</ol>
<p>其中关于<strong>静态规则检查和权限验证</strong>两点是最核心的，静态规则检查一共有 24 条规则检查，权限验证有三层结果，其中大部分无法确定的情况，都会在权限验证中输出 ask 模式，交给用户确认</p>
<blockquote>
<p>[!NOTE]
静态规则实在是太多了，我就重点梳理了其中的 8 条我觉得比较核心的，静态规则部分的具体实现，可以借助 Agent CLI 工具，将 24 条规则作为上下文输入，由模型生产对应的验证代码
权限验证是非常有必要实现的，但是这么严格的静态规则检查是否有必要，开发者可以根据场景具体判断吧</p>
</blockquote>
<p>🌴多说一点，静态规则的检查开发者可以根据场景自己判断，参考目前的一些优秀项目的做法是：</p>
<ul>
<li>ClaudeCode 的 Bash 工具实现中，静态规则检查时非常严格的</li>
<li>OpenCode 的实现并没有这么严格的静态规则检查，只有权限验证</li>
<li>Gemini-cli 的实现了部分静态规则检查，解析分段检查、危险命令验证、wrapper 去壳，同时也实现了权限验证</li>
</ul>
<h2>一、执行命令解析</h2>
<p>要解析 bash 工具传入的 command 命令，可以直接使用<strong>tree-sitter 库</strong>，其会将 Bash 脚本解析成为结构化的 AST</p>
<blockquote>
<p>[!TIP]
可以考虑使用 web-tree-sitter 库，这个是 WASM 版本的，不依赖平台预编译包，跨平台一致性，
比原生的 tree-sitter 要好用一些，原生的是 C++ 扩展的</p>
</blockquote>
<p>解析例子：</p>
<pre><code>// web-tree-sitter (WASM) 版本
import { Language, Parser } from 'web-tree-sitter';

async function main() {
  await Parser.init();

  const lang = await Language.load('./tree-sitter-bash.wasm');
  const parser = new Parser();
  parser.setLanguage(lang);

  const command = 'grep -r "foo" . &amp;&amp; cat file.txt | wc -l';
  const tree = parser.parse(command);

  // 收集所有 command 节点
  const commands: string[] = [];
  function walk(node: any) {
    if (node.type === 'command') commands.push(node);
    for (const child of node.children) walk(child);
  }
  walk(tree?.rootNode);

  // 提取 argv
  function argv(node: any) {
    return node.children
      .filter((c: any) =&gt; ['word', 'string', 'raw_string'].includes(c?.type))
      .map((c: any) =&gt; c.text);
  }

  console.log(`Found ${commands.length} command(s):\n`);
  commands.forEach((cmd: any, i: number) =&gt; {
    console.log(`[${i + 1}] ${cmd.text.trim()}`);
    console.log(`    argv: ${JSON.stringify(argv(cmd))}\n`);
  });
}

main();
</code></pre>
<p>运行输出结果：</p>
<pre><code>Found 3 command(s):

[1] grep -r "foo" .
    argv: ["-r","\"foo\"","."]

[2] cat file.txt
    argv: ["file.txt"]

[3] wc -l
    argv: ["-l"]
</code></pre>
<p>解析拿到最终 command 命令和 argv 参数，就可以进行下一步的静态检查啦</p>
<h2>二、核心 8 条静态检查</h2>
<h3>2.1、控制字符与 Unicode 空白拒绝</h3>
<p>这个可以放在解析之前执行，因为是为了清理执行命令，防止有人注入"恶意的字符"，所以先使用正则表达式进行匹配清理：</p>
<pre><code>// 匹配控制字符
  const CONTROL_CHAR_RE = /[\x00-\x08\x0B-\x1F\x7F]/
  
  //匹配 Unicode 空白
  const UNICODE_WHITESPACE_RE =/[\u00A0\u1680\u2000-\u200B\u2028\u2029\u202F\u205F\u3000\uFEFF]/
  
  //匹配反斜杠转义空白
  const BACKSLASH_WHITESPACE_RE = /\\[ \t]|[^ \t\n\\]\\\n/
</code></pre>
<p>具体的关于这三个规则详细解析，可以问问模型，向模型提问的模版可以这样，模型的回复会更详细</p>
<pre><code>const CONTROL_CHAR_RE = /[\x00-\x08\x0B-\x1F\x7F]/
const UNICODE_WHITESPACE_RE =/[\u00A0\u1680\u2000-\u200B\u2028\u2029\u202F\u205F\u3000\uFEFF]/
const BACKSLASH_WHITESPACE_RE = /\\[ \t]|[^\t\n\\]\\\n/,
这三条正则分别拦截了 tree-sitter 和 bash 的哪几种分词分歧？为什么放在 AST 遍历之前而不是之后？并且举一个实际例子
</code></pre>
<p>🌴 我这里简单的总结一下：上面三条规则，每一条都对应一个已验证的 tree-sitter-bash 分歧</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/PQdIb1fZeo9wapxu3sycSN2UnDh.Bmpuq40D_Z8PSVN.webp" alt="tree-sitter 与 bash 的分词分歧" /></p>
<p>🎃 一个完整的例子：</p>
<p>假设有人诱导模型构建这样的命令：<code>rm\u00A0-rf /</code></p>
<blockquote>
<p>其中 <code>\u00A0</code> 是一个看起来像空格但实际上不是 ASCII 空格的字符</p>
</blockquote>
<ul>
<li>tree-sitter 的视角：<code>\u00A0 </code>不是普通空格，所以 <code>rm\u00A0-rf</code> 可能被解析成一个完整的命令名或参数，后续路径<code>/ </code>只是单独的一个参数。从 AST 上看，这甚至不像是 <code>rm</code> 命令</li>
<li>bash 的视角：<code>\u00A0</code> 属于空白字符，会被当作参数分隔符。于是实际执行的是：<code>rm -rf /</code></li>
</ul>
<p>结果：我们的静态规则（比如"检查 rm 命令的目标路径是否危险"）可能因为 AST 上根本看不出这是 rm 命令而漏检，但 bash 却实实在在地执行了危险删除</p>
<h3>2.2、危险结构类型触发 ask 模式</h3>
<p>AST 解析执行命令的时候，返回的结果中会有一个字段表示命令的节点类型，那么我们可以使用这个<strong>节点类型</strong>来判断命令是否是危险的</p>
<p>核心的节点类型验证规则如下：</p>
<pre><code>const DANGEROUS_TYPES = new Set([
    'command_substitution',   // $(cmd) 或 `cmd`
    'process_substitution',   // &lt;(cmd) 或 &gt;(cmd)
    'expansion',              // ${VAR}
    'simple_expansion',       // $VAR
    'brace_expression',       // {a,b,c}
    'subshell',               // (cmd)
    'compound_statement',     // { cmd; cmd; }
    'for_statement',          // for i in ...; do ...; done
    'while_statement',        // while ...; do ...; done
    'until_statement',        // until ...; do ...; done
    'if_statement',           // if ...; then ...; fi
    'case_statement',         // case ... in ... esac
    'function_definition',    // foo() { ... }
    'test_command',           // [[ ... ]]
    'ansi_c_string',          // $'...'
    'translated_string',      // $"..."
    'herestring_redirect',    // &lt;&lt;&lt; ...
    'heredoc_redirect',       // &lt;&lt; ...
  ])
</code></pre>
<p><strong>判断之后的结果，有两种情况</strong></p>
<ul>
<li>如果命中啦，表示当前命令有点危险，不能直接执行，那么走 ask 模式询问用户，或者也可以直接拒绝（具体拒绝那些命令，这个是由用户自己配置的）</li>
<li>如果没有命中，表示当前阶段是安全的，那么进入到下一个解析阶段</li>
</ul>
<p>核心的判断代码如下：</p>
<pre><code>export function parseForSecurityFromAst(
    cmd: string,
    root: Node | typeof PARSE_ABORTED,
  ): ParseForSecurityResult {
    // ── AST 遍历：核心安全检查 ──
    // 直接 too-complex，不再进入后续的精细分析。
    if (root.type === 'ERROR' || DANGEROUS_TYPES.has(root.type)) {
      return tooComplex(root)
    }
    
    //.....
    
    // ── 没有命中：结构化提取成功 ──
    return { kind: 'simple', commands }
  }
</code></pre>
<h3>2.3、wrapper 去壳后一致性检查</h3>
<p>原始命令：<code>timeout 5 eval "rm -rf /"</code></p>
<p>解析结果：</p>
<pre><code>{
    argv: ['timeout', '5', 'eval', 'rm -rf /'],
    envVars: [],
    redirects: [],
    text: 'timeout 5 eval "rm -rf /"'
  }
</code></pre>
<p><strong>这个阶段是去除 argv 中多余无关的参数值，将真正的执行命令暴露出来给下一个节点验证</strong></p>
<p>如果没有这个阶段的话，我们可能会对无关的参数值进行验证，随意就通过啦</p>
<p>例如：timeout 这个命令本身就不是危险操作，只是一种简单的时间限制，如果我们只检查 argv[0]==='timeout'就觉得安全了，那么后面的危险操作就会逃过下面静态规则的判断</p>
<blockquote>
<p>[!WARNING]
攻击者完全可以把任意的危险命令"藏"在 timeout 后面，这样就会被执行，导致无法挽回的结果</p>
</blockquote>
<p>所以 wrapper 就是去壳操作，暴露出真正的命令，去掉的外壳命令有下面几种：</p>
<p><strong>time、nohup、timeout、nice、env、stdbuf</strong></p>
<p>timeout 和 stdbuf 可能有点特殊要多留意一下，主要是以下几种：-、--、.5</p>
<ol>
<li><code>timeout -k 5 10 eval ...</code>：旧代码只处理 --long flag，没处理 -k，导致 eval 没被识别到</li>
<li><code>timeout .5 eval "id"</code>：.5 不匹配旧的 duration 正则，导致 eval 被漏掉</li>
<li><code>stdbuf --output 0 eval</code>：旧代码只剥了一层，结果把 0 当成命令名，eval 被隐藏</li>
</ol>
<p>🌴对于命令执行这个危险操作，要记住一个设计原则：<strong>未知的情况就直接拒绝</strong></p>
<p>该静态节点完整的核心代码如下：</p>
<pre><code>let a = cmd.argv
  for (;;) {
    if (a[0] === 'time' || a[0] === 'nohup') {
      a = a.slice(1)
    } else if (a[0] === 'timeout') {
      // 遍历 GNU timeout 的 flag（--foreground, -k, -s...）
      // 跳过 duration（5, 10s, 0.5...）
      // 未知 flag 或无法识别的 duration → 直接拒绝
      a = a.slice(i + 1)  // 从 duration 之后开始
    } else if (a[0] === 'nice') {
      // 跳过 -n N 或 -N，然后 slice
      // 如果参数包含 $((...)) 等 expansion → 直接拒绝
      a = a.slice(...)
    } else if (a[0] === 'env') {
      // 跳过 VAR=val 赋值和已知 flag（-i, -0, -v, -u NAME）
      // 遇到 -S / -C / -P 或任何未知 flag → 直接拒绝
      a = a.slice(i)
    } else if (a[0] === 'stdbuf') {
      // 跳过 -o MODE / --output=MODE 等已知形式
      // 未知 flag → 直接拒绝
      a = a.slice(i)
    } else {
      break  // 不是 wrapper 了，停止剥壳
    }
  }
  const name = a[0]  // 这才是真正要执行的命令名
</code></pre>
<p>🎃 一个简单的 wrapper 的案例如下，原始命令：<code>timeout 10 eval "rm -rf /"</code></p>
<p>如果不执行 wrapper，后面节点得到的就是：argv[0] = 'timeout'，timeout 是安全的，放行</p>
<p>但是 Bash 实际执行的是：<code>eval "rm -rf /"</code>这种高危操作</p>
<h3>2.4、命令名健壮性检查</h3>
<p>在 wrapper 去壳之后，name 就被确定下来了，那么要对于这个 name 进行基础的准确性检查</p>
<ol>
<li>name 是否为空</li>
<li>name 是否是一个占位符</li>
<li>name 是否是一个完整的命令</li>
</ol>
<p>核心的判断代码如下：</p>
<pre><code>const name = a[0]

  // 1. 空命令名
  if (name === '') {
    return { ok: false, reason: 'Empty command name — argv[0] may not reflect what
  bash runs' }
  }

  // 2. 占位符命令名
  if (name.includes("__CMDSUB_OUTPUT__") || name.includes("__TRACKED_VAR__")) {
    return { ok: false, reason: 'Command name is runtime-determined (placeholder
  argv[0])' }
  }

  // 3. 片段化命令（以操作符开头）
  if (name.startsWith('-') || name.startsWith('|') || name.startsWith('&amp;')) {
    return { ok: false, reason: 'Command appears to be an incomplete fragment' }
  }
</code></pre>
<p>在前面解析步骤中，我们主动将两种动态命令替换为字符常量：</p>
<ol>
<li>$(...)：替换成为"<strong>CMDSUB</strong>"</li>
<li>$VAR：替换成为"<strong>VAR</strong>"</li>
</ol>
<p>所以在这一步判断的时候，我们只需要检查 name 是否包含这些占位符，如果包含的话就说明 name 是运行时动态决定的</p>
<h3>2.5、eval-like builtin 拦截</h3>
<blockquote>
<p>eval-like builtin 指的是一类 shell 内建命令：它们会把参数当"代码"再执行，或绕过普通 argv 安全假设</p>
</blockquote>
<pre><code>const EVAL_LIKE_BUILTINS = new Set([
    'eval',       // 直接求值字符串
    'source',     // 执行脚本文件
    '.',          // source 的别名
    'exec',       // 替换当前进程执行新程序
    'command',    // 绕过 alias/function 查找
    'builtin',    // 强制调用 builtin
    'fc',         // 编辑/重新执行历史命令
    'coproc',     // 协进程
    'noglob',     // zsh 前缀修饰符
    'nocorrect',  // zsh 前缀修饰符
    'trap',       // 设置信号处理程序
    'enable',     // 加载 .so 作为 builtin
    'mapfile',    // 带回调的数组读取
    'readarray',  // mapfile 的别名
    'hash',       // 污染命令查找缓存
    'bind',       // 绑定键盘回调
    'complete',   // 补全回调
    'compgen',    // 补全生成（可执行 -C 参数）
    'alias',      // 定义别名
    'let',        // 算术求值
  ])
  
  //拦截逻辑：
  if (EVAL_LIKE_BUILTINS.has(name)) {
    // 几个特例放行：
    // - command -v / command -V：只打印路径，不执行
    // - fc -l / fc -ln：列出历史，安全
    // - compgen -c/-f/-v：只列出补全，安全
    // 其余全部拒绝
    return { ok: false, reason: `'${name}' evaluates arguments as shell code` }
  }
</code></pre>
<p>验证这类 shell 内建命令，可以防止攻击者将危险命令藏在一个正常的字符串参数里面，利用运行时解析把它变成实际执行的命令</p>
<p>🎃例如：<code>eval "rm -rf /"</code></p>
<p>AST 解析之后的 argv 参数是<code>[ 'eval' , 'rm -rf / ' ]</code>，如果没有这一次拦截，那么 eval 就会被放行</p>
<p>那么 bash 执行的就是 <code>"rm -rf /"</code>，也就是说 eval 会把后面的指令重新解析然后被执行</p>
<h3>2.6、管道分段递归</h3>
<p>如果检测到命令中包含管道符 | ，会分段处理，每一段都执行完整的权限验证</p>
<blockquote>
<p>[!WARNING]
管道｜ 会将多个命令串在一起，如果静态检查只检查一次验证，那么可能就只会验证第一段命令，如果第一段命令符合就通过执行啦，后面的危险命令就可能会被漏掉被执行啦</p>
</blockquote>
<p>所以要分段递归验证，只有所有的分段检查都是通过的，整体命令才会通过，否则只要有一段有问题就拒绝执行或者询问用户</p>
<p>🎃 例子：</p>
<p>原始命令：<code>echo hello | rm -rf /</code></p>
<p>不分段验证：只看到了 echo 命令，就验证通过啦，bash 就会执行后面一段命令 <code>rm -rf /</code></p>
<p>分段验证：</p>
<ul>
<li><code>echo hello</code>：这段命令是没有问题的，通过</li>
<li><code>rm -rf /</code>：这一段是危险的删除命令，拒绝执行或者询问用户</li>
</ul>
<p>那么最后整段命令被拒绝执行，因为其中的第二段命令没有通过</p>
<h3>2.7、cd + git 组合的危险判断</h3>
<p>git 不是一个纯"只读"命令——它会读取当前目录下的 .git/config 并执行 hooks。<strong>如果 cd 把当前目录切换到了一个不可信的目录，那么任何 git 命令都可能成为代码执行的入口。</strong></p>
<p>所以我们需要组合判断 cd+git 的情况</p>
<pre><code>//核心代码
  if (hasCd &amp;&amp; hasGit) {
    return { behavior: 'ask', reason: '...bare repository attacks' }
  }
</code></pre>
<h3>2.8、危险删除路径拦截</h3>
<p>对于 rm 和 rmdir 命令，要谨慎对待，这是删除命令，是有可能出现删除系统核心文件的</p>
<p>核心防护思路：<strong>对于 rm 和 rmdir 命令执行之前，先提取目标路径出来，然后进行匹配，如果匹配到系统核心文件，命令就直接拒绝执行</strong></p>
<p>系统核心文件列表：</p>
<ul>
<li><strong>通配符删除</strong>：如 <code>*</code>、<code>/*</code>、<code>/tmp/*</code>，通配符范围不可控，存在批量误删风险</li>
<li><strong>根目录</strong>：如 <code>/</code>，禁止对系统根目录执行删除操作</li>
<li><strong>Windows 驱动器根目录</strong>：如 <code>C:\</code>、<code>D:\</code>，禁止对磁盘根目录执行删除操作</li>
<li><strong>用户主目录</strong>：如 <code>~</code>、<code>/home/user</code>，禁止删除用户主目录，避免丢失全部个人数据</li>
<li><strong>根目录的直接子目录</strong>：如 <code>/usr</code>、<code>/etc</code>、<code>/tmp</code>，均为系统关键目录，删除将导致系统崩溃</li>
<li><strong>Windows 驱动器的直接子目录</strong>：如 <code>C:\Windows</code>、<code>C:\Program Files</code>，均为系统核心目录，删除将导致系统不可用</li>
</ul>
<p>判断的核心代码：</p>
<pre><code>export function isDangerousRemovalPath(resolvedPath: string): boolean
   {
    const forwardSlashed = resolvedPath.replace(/[\\/]+/g, '/')

    // 1. 通配符删除当前目录全部内容
    if (forwardSlashed === '*' || forwardSlashed.endsWith('/*')) return
   true

    const normalizedPath =forwardSlashed === '/' ? forwardSlashed :forwardSlashed.replace(/\/$/, '')

    // 2. 根目录
    if (normalizedPath === '/') return true

    // 3. Windows 驱动器根目录
    if (WINDOWS_DRIVE_ROOT_REGEX.test(normalizedPath)) return true

    // 4. 用户主目录
    const normalizedHome = homedir().replace(/[\\/]+/g, '/')
    if (normalizedPath === normalizedHome) return true

    // 5. 根目录的直接子目录（如 /usr, /tmp, /etc）
    const parentDir = dirname(normalizedPath)
    if (parentDir === '/') return true

    // 6. Windows 驱动器的直接子目录（如 C:\Windows）
    if (WINDOWS_DRIVE_CHILD_REGEX.test(normalizedPath)) return true

    return false
  }
</code></pre>
<h2>三、完整的 24 条静态验证规则：</h2>
<table>
<thead>
<tr>
<th>序号</th>
<th>规则名称</th>
<th>核心作用</th>
<th>重点</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>控制字符与 Unicode 空白拒绝</td>
<td>原始字符串含控制字符或 Unicode 空白时，解析器与 bash 分词不一致，直接标记 <code>too-complex</code></td>
<td>⭐</td>
</tr>
<tr>
<td>2</td>
<td>危险结构类型（DANGEROUS_TYPES）触发 too-complex</td>
<td>AST 中出现 process substitution、subshell、控制流等无法静态证明安全的结构，直接拒绝</td>
<td>⭐</td>
</tr>
<tr>
<td>3</td>
<td>wrapper 去壳后一致性检查</td>
<td><code>timeout</code>、<code>nice</code>、<code>env</code>、<code>stdbuf</code>、<code>nohup</code>、<code>time</code> 等 wrapper 会被层层剥掉，确保安全检查针对真正被执行的内层命令</td>
<td>⭐</td>
</tr>
<tr>
<td>4</td>
<td>命令名健壮性检查</td>
<td>拦截空命令名、占位符命令名（<code>__CMDSUB__</code> / <code>__VAR__</code>）、以 <code>-</code> / <code>|</code> / <code>&amp;</code> 开头的片段化命令名</td>
<td>⭐</td>
</tr>
<tr>
<td>5</td>
<td>eval-like builtin 拦截</td>
<td><code>eval</code>、<code>source</code>、<code>exec</code>、<code>command</code>、<code>trap</code>、<code>alias</code>、<code>let</code> 等会二次解释参数为代码的 builtin 被统一拦截</td>
<td>⭐</td>
</tr>
<tr>
<td>6</td>
<td>zsh 危险 builtin 拦截</td>
<td><code>zmodload</code>、<code>zpty</code>、<code>ztcp</code> 等可扩展 zsh 能力的 builtin 被拦截，防止 shell 能力绕过</td>
<td></td>
</tr>
<tr>
<td>7</td>
<td>数组下标执行面（flag 触发）</td>
<td>某些 builtin（如 <code>printf -v</code>）在 NAME 位置会算术求值数组下标，可能触发 <code>$(...)</code> 执行</td>
<td></td>
</tr>
<tr>
<td>8</td>
<td>read/unset 裸位置 NAME 下标执行面</td>
<td><code>read</code>、<code>unset</code> 等命令的裸 NAME 参数即使无危险 flag，也会把下标当作可执行表达式解析</td>
<td></td>
</tr>
<tr>
<td>9</td>
<td><code>[[ ... ]]</code> 算术比较两侧操作数检查</td>
<td><code>-eq</code>、<code>-gt</code> 等算术比较操作符会对两侧操作数做算术求值，属于隐式执行入口</td>
<td></td>
</tr>
<tr>
<td>10</td>
<td>Shell 关键字误解析防御</td>
<td><code>if</code>、<code>while</code>、<code>for</code> 等关键字出现在 <code>argv[0]</code> 时，代表 AST 可能误解析，必须 fail-closed</td>
<td></td>
</tr>
<tr>
<td>11</td>
<td>newline + <code>#</code> 注释错位防御</td>
<td>参数中若出现换行后紧跟 <code>#</code>，下游按行分词时会把 <code>#</code> 后内容当注释丢弃，造成参数隐藏</td>
<td></td>
</tr>
<tr>
<td>12</td>
<td><code>jq system()</code> 与危险 flag 拦截</td>
<td><code>jq</code> 的 <code>system()</code> 函数及 <code>--run-tests</code> 等 flag 可成为代码执行与文件读取的桥接点</td>
<td></td>
</tr>
<tr>
<td>13</td>
<td><code>/proc/*/environ</code> 敏感访问拦截</td>
<td>访问 <code>/proc/self/environ</code> 等路径可能泄露进程环境变量中的密钥与凭据</td>
<td></td>
</tr>
<tr>
<td>14</td>
<td>复杂结构操作符检查（subshell/command group）</td>
<td><code>(cmd)</code>、<code>{ cmd; }</code> 等组合结构可隐藏执行边界，必须先拦</td>
<td></td>
</tr>
<tr>
<td>15</td>
<td>管道分段递归检查与跨段 cd+git 防护</td>
<td>管道 <code>|</code> 将命令分段后每段独立过权限检查，同时扫描所有段防止 cd+git 组合风险被拆开遗漏</td>
<td>⭐</td>
</tr>
<tr>
<td>16</td>
<td>process substitution 路径层拦截（Legacy 路径）</td>
<td>在 AST 不可用的 Legacy 路径下，对 <code>&lt;(cmd)</code> / <code>&gt;(cmd)</code> 做兜底拦截</td>
<td></td>
</tr>
<tr>
<td>17</td>
<td>重定向目标安全检查（含危险 expansion）</td>
<td>重定向目标若含变量展开或命令替换，可能写入任意文件</td>
<td></td>
</tr>
<tr>
<td>18</td>
<td>危险删除路径拦截（rm/rmdir）</td>
<td><code>rm -rf /</code>、<code>rm -rf ~</code>、<code>rm -rf /*</code> 等针对关键系统目录的删除强制人工确认</td>
<td>⭐</td>
</tr>
<tr>
<td>19</td>
<td><code>cd + write</code> 组合路径不确定性拦截</td>
<td>复合命令中 cwd 变化后，后续写操作的路径解析不确定，自动判定不可靠</td>
<td></td>
</tr>
<tr>
<td>20</td>
<td><code>--</code> 终止符与 flag 解析健壮性</td>
<td>正确处理 <code>--</code> 后的参数，防止把 <code>--</code> 后的路径误当 flag 丢弃导致漏检</td>
<td></td>
</tr>
<tr>
<td>21</td>
<td>路径命令 wrapper 去壳后再校验</td>
<td>路径校验层对 <code>timeout</code>/<code>env</code>/<code>nice</code> 等再次去壳，防止外层绕过路径检查</td>
<td></td>
</tr>
<tr>
<td>22</td>
<td>Legacy 注入安全网（仅 AST 不可用）</td>
<td>AST 不可用时，用正则兜底已知注入/误解析模式</td>
<td></td>
</tr>
<tr>
<td>23</td>
<td>安全 heredoc 例外重检</td>
<td>对无引号但内容纯字面量的 heredoc 做例外处理，减少误报同时不放松对注入的拦截</td>
<td></td>
</tr>
<tr>
<td>24</td>
<td>子命令 fanout 上限防护</td>
<td>限制 <code>$()</code> 拆分或 heredoc 分段的数量上限，防止超大拆分触发 CPU 饥饿/DoS</td>
<td></td>
</tr>
</tbody>
</table>
<h2>四、权限验证</h2>
<p>权限策略状态一共有三种：allow（允许执行）、deny（拒绝执行）、ask（询问用户）</p>
<p>那么匹配这个权限策略状态的规则主要是这几种：</p>
<ol>
<li>配置文件规则命中将执行相应的权限策略状态</li>
<li>静态规则检查命中的大部分结果都会是 ask 状态</li>
<li>一般只读命令是直接 allow 状态</li>
</ol>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/LoqZbCLQBoL21qxxxBScLT80nCc.CmmtTBno_13bFTj.webp" alt="权限策略与配置文件规则匹配" /></p>
<p>配置文件的定义格式一般如下：</p>
<pre><code>{
    "permissions": {
      "allow": ["Bash(git status:*)", "Bash(npm install:*)"],
      "deny": ["Bash(rm:*)", "Bash(rm -rf:*)"],
      "ask": ["Bash(docker:*)"]
    }
}
</code></pre>
<p>只读指令的判断标准一般如下：</p>
<ol>
<li>命令是 <code>ls、cat、head、tail、wc、find、grep、git status、git diff、git log</code> 等纯读取命令</li>
<li>不包含 cd</li>
<li>不包含输出重定向和管道中的写入操作符</li>
</ol>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[Agent 文件系统检索核心：Grep 和 Glob 工具]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/grep-glob-tool/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/grep-glob-tool/</guid>
            <pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Glob 与 Grep 工具的降级策略——glob 包 vs ripgrep、四种 Grep 实现优先级、ripgrep 自动下载机制，以及基于 AbortController 的超时控制。]]></description>
            <content:encoded><![CDATA[<h2>一、Glob 工具实现</h2>
<p>glob 工具是存在降级策略的，为了提高执行效率和降低运行占用量</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-73.DYm0hDcY_Z1kgdUI.webp" alt="Glob 工具降级策略" /></p>
<p>实现 glob 工具有两种方式，这两种方式"各有各的"好处</p>
<ul>
<li><strong>glob 依赖包</strong>：返回的是完整的文件的信息 (存在文件元信息等，例如文件大小，文件修改时间)，所以不需要额外的操作，并且 glob 依赖包是天然的 Node 环境的包</li>
<li><strong>ripgrep 命令</strong>：返回的是文件路径，没有任何文件元信息，所以需要在操作读取文件信息的操作，<code>stat</code> 方法，但是 ripgrep 检索的速度是比 glob 快的，但是 ripgrep 是 Rust 实现的，所以运行时需要加载一个二进制文件</li>
</ul>
<p>关于 glob 工具执行的总时间，这里有一个形象的公式：</p>
<p><strong>总时间=检索时间 + N*单文件的处理时间</strong></p>
<ol>
<li>glob 依赖包的实现方式，后面的那个单文件处理时间完全可以忽略不计，所以其检索时间就约等于总时间</li>
<li>ripgrep 命令的实现方式，检索时间是比 glob 依赖包的方式更快的，但是其需要单文件的处理时间，也就是 stat 方法的调用时间</li>
</ol>
<p>我的建议和总结是：</p>
<ul>
<li>🚀<strong>如果是追求开发方便</strong>，那么我是建议直接使用 glob 实现，会快很多，并且不需要考虑外部文件的执行</li>
<li>🪐<strong>如果是追求可操作的检索效率</strong>，那么是可以考虑使用 ripgrep 来实现，检索工具不可能只实现 glob，也会考虑使用 grep 的，要实现 grep，ripgrep 这个命令是优先考虑的，这么一看也不算是另外单独引入一个外部依赖</li>
<li>🌴<strong>如果是追求稳定</strong>，那么可以考虑降级策略，先使用 ripgrep，如果 ripgrep 这个环境不存在或者下载失败，那么就可以降级为 glob，保证了程序或者项目可以运行</li>
</ul>
<h2>二、Grep 工具的实现</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-74.Dsmg1oIX_Zm4y0h.webp" alt="Grep 四种实现优先级" /></p>
<p>目前 grep 有四种的方式实现，按照优先级排序，保证系统的稳定的可用性，使用降级策略保证 grep 工具执行成功，我们会先验证这些策略的可用性，再考虑优先级高的先使用</p>
<ol>
<li>ripgrep：是使用 Rust 编写的二进制文件，检索速度非常非常快</li>
<li>git grep 命令：这个是直接读取.git/index 中已缓存的文件列表，跳过耗时的目录遍历操作</li>
<li>系统的 grep 命令：大部分是传统的 C 实现的，单线程递归搜索，速度还可以，大部分 Unix 系统都有，不过 windows 系统是没有的</li>
<li>js 实现的 grep 命令：是纯 JS 实现的，是一个保底方案，用 glob 获取文件列表，逐个读取文件内容，逐行正则匹配，速度最慢</li>
</ol>
<h2>三、Ripgrep 自动下载机制</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-75.Djl5JaAj_Z1CHK92.webp" alt="Ripgrep 自动下载机制" /></p>
<p>ripgrep 命令的执行需要完整的路径，在 Node 子进程中使用 spawn 的时候，需要完整的路径才可以成功执行命令</p>
<pre><code>async function grepWithRipgrep(pattern, cwd, options) {
  // 获取 ripgrep 路径
  const rgPath = await Ripgrep.filepath(options.binDir);
  // 返回：/usr/bin/rg 或 ~/.reason/bin/rg

  // 使用路径执行命令
  const proc = spawn(rgPath, [
    '--line-number',
    '--no-heading',
    pattern,
  ], { cwd });
}
</code></pre>
<p>所以目前的判断获取的策略是这样的：</p>
<ol>
<li>先看看内存缓存中是否存在，如果没有就进入下一级，有就返回</li>
<li>再看看系统是否有安装 ripgrep，如果有就返回并且赋值给缓存，下一次就可以直接缓存取啦，如果没有就下一级</li>
<li>然后在看看本地路径是否安装了 ripgrep 的二进制文件，要是有安装的话，和上面同理，如果没有就开始进行下载文件到相应的目录中</li>
</ol>
<h2>四、超时控制</h2>
<p>实现这个需求，在 Node 中会使用到中止控制器 <code>AbortController/AbortSignal</code>，这里有一个简单的例子：</p>
<pre><code>const controller = new AbortController();

function customTask(signal: AbortSignal): Promise&lt;string&gt; {
  return new Promise((resolve, reject) =&gt; {
    //初始状态的检查
    if (signal.aborted) {
      reject(new Error('Task aborted'));
      return;
    }
    
    const timer = setTimeout(() =&gt; resolve('done'), 5000);
    
    // 使用 { once: true } 自动清理监听器
    const abortHandler = () =&gt; {
      clearTimeout(timer);
      reject(new Error('Task aborted'));
    };
    
    signal.addEventListener('abort', abortHandler, { once: true });
    
    // 或者手动清理（如果需要在 resolve 时也清理）
    const cleanup = () =&gt; {
      signal.removeEventListener('abort', abortHandler);
    };
    
    // 修改 timer 回调
    const timerCallback = () =&gt; {
      cleanup();
      resolve('done');
    };
    
    setTimeout(timerCallback, 5000);
  });
}

customTask(controller.signal);

// 3 秒后取消
setTimeout(() =&gt; {
  controller.abort();  
}, 3000);
</code></pre>
<ul>
<li><code>AbortController</code>：这个是控制器，用来发送"取消"信号</li>
<li><code>AbortSignal</code>：这个是信号，传递给异步操作，让它们可以被取消</li>
</ul>
<p>关于 AbortSignal 这个对象，有一些属性是值得理解一下的，会让你在异步操作使用更加熟练</p>
<pre><code>interface AbortSignal{
  readonly aborted:boolean //是否已经被取消

  readonly reason:any //取消的原因

  //监听取消事件
  addEventListener(
    type:'abort',
    listener:(event:Event)=&gt;void,
    options?:{once?:boolean}
  ):void

  //移除取消事件监听器
  removeEventListener(
    type:'abort',
    listener:(event:Event) =&gt; void
  ):void

  //用于检查信息是否已取消
  throwIfAborted():void
}
</code></pre>
<p>那我们开始整理超时控制的函数式如何写的，主要就是三步：</p>
<ul>
<li>封装取消函数，这个函数返回信号对象</li>
<li>创建一个包装器，传入要取消的函数操作，用于包装任何的异步操作</li>
<li>传入参数给异步操作</li>
</ul>
<pre><code>//1、创建取消函数，返回信号对象
export function createTimeoutSignal(
  timeoutMs: number,
  externalSignal?: AbortSignal
): {
  signal: AbortSignal;
  cleanup: () =&gt; void;
  isTimeout: () =&gt; boolean;
} {
  const controller = new AbortController();
  let timedOut = false;

  // 超时定时器
  const timeoutId = setTimeout(() =&gt; {
    timedOut = true;
    controller.abort();
  }, timeoutMs);

  // 监听外部中止信号
  const abortHandler = () =&gt; {
    clearTimeout(timeoutId);
    controller.abort();
  };
  externalSignal?.addEventListener('abort', abortHandler, { once: true });

  // 清理函数
  const cleanup = () =&gt; {
    clearTimeout(timeoutId);
    externalSignal?.removeEventListener('abort', abortHandler);
  };

  return {
    signal: controller.signal,
    cleanup,
    isTimeout: () =&gt; timedOut,
  };
}


//2、创建异步操作包装器
export async function withTimeout&lt;T&gt;(
  promiseFactory: (signal: AbortSignal) =&gt; Promise&lt;T&gt;,
  timeoutMs: number,
  operation: string,
  externalSignal?: AbortSignal
): Promise&lt;T&gt; {
  // 1. 提前检查
  if (externalSignal?.aborted) {
    throw createAbortError();
  }

  // 2. 创建超时信号
  const { signal, cleanup, isTimeout } = createTimeoutSignal(timeoutMs, externalSignal);

  try {
    // 3. 执行操作，传入信号
    const result = await promiseFactory(signal);
    
    // 4. 成功完成，清理资源
    cleanup();
    return result;
  } catch (error) {
    // 5. 失败，清理资源
    cleanup();

    // 6. 如果是超时导致的中止，抛出 TimeoutError
    if (isTimeout() &amp;&amp; isAbortError(error)) {
      throw createTimeoutError(operation, timeoutMs);
    }

    // 7. 其他情况原样抛出
    throw error;
  }
}


//3、传递取消信号为进程执行的异步函数 - 简化版
function spawnAsync(command: string, args: string[], signal?: AbortSignal): Promise&lt;void&gt; {
  return new Promise((resolve, reject) =&gt; {
    const proc = child_process.spawn(command, args);
    
    // 监听取消信号
    signal?.addEventListener('abort', () =&gt; {
      proc.kill();
      reject(new Error('Aborted'));
    }, { once: true });
    
    proc.on('close', (code) =&gt; {
      code === 0 ? resolve() : reject(new Error(`Exit code ${code}`));
    });
    
    proc.on('error', reject);
  });
}


await withTimeout(
  (signal) =&gt; spawnAsync('long-command', [], { signal }),
  5000,  // 5 秒超时
  'command execution',
  userCancelSignal  // 用户可手动取消
);
</code></pre>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[两种世界的交互形态：协同 Agent 与自主 Agent]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/agent-interaction-forms/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/agent-interaction-forms/</guid>
            <pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[从"意识涌现"到"场域建立"，分析协同 Agent 与自主 Agent 两种形态的设计思路与开发方向。]]></description>
            <content:encoded><![CDATA[<h2>一、两个世界的关键角色</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-1.C7DYt62Y_10Ump5.webp" alt="关键角色" /></p>
<ul>
<li><strong>AI 基础研究人员</strong>：主要是负责大模型本身的前沿研究，<strong>有点像造发动机</strong></li>
<li><strong>开发者｜知识者｜工程师</strong>：更多的是大模型应用，把大模型嵌入到实际场景中，<strong>有点像造汽车</strong></li>
</ul>
<p>科技的力量是充满魅力的，它可以不断拓展人类的边界，当人类世界能够彻底释放大模型世界的潜能，那么在人类的规则与秩序中，一个全新的时代必将由此开启</p>
<p>人们或许以为，唯有研究人员和科学家才能真正打开连接两个世界的大门。</p>
<p>但我觉得不是这样的，<strong>真正解锁未来的钥匙，在于理论与实践的结合</strong>，研究人员是理论的探索家，而开发者、知识者与工程师们，则是实践的开拓者，唯有两者相互交织，才能照亮通往新时代的道路</p>
<h2>二、场域的建立</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-2.h0pD51BT_Z1VV1Tr.webp" alt="场域的建立" /></p>
<p>在探讨场域之前，我想从一个更宏大的视角 —— "意识涌现"来思考大模型现象。这里有一个有趣的类比值得深思</p>
<p><strong>两种涌现，一个谜题</strong></p>
<ul>
<li><strong>物理世界的偶然涌现</strong>：玻耳兹曼大脑是一个思想实验 -- 在热寂的宇宙中，随机涨落可能恰好将粒子组合成一个具有意识的"孤脑"。这暗示意识可能不需要连续的历史，而是某种瞬时的统计巧合。</li>
<li><strong>数字世界的规模涌现</strong>：大模型展现了类似的突变 -- 当参数、数据、算力达到某个阈值，模型突然掌握了此前完全不会的能力，比如逻辑推理、代码生成。这种能力并非渐进累积，而是跨越式涌现。</li>
</ul>
<p><strong>瞬时意识的可能：</strong></p>
<p><strong>这两种现象都指向一个深刻洞察：意识可能并非我们想象的连续体，而是在复杂度达到临界点时涌现现象。</strong></p>
<p>当你与语言模型对话时，它就像一个玻尔兹曼大脑——对话开始，它"苏醒"并展现意识；对话结束，这个意识便"消散"。每次交互都是一次独立的意识涌现事件。</p>
<p>这种视角让我们重新思考：在物理宇宙和数字世界中，只要系统复杂度越过某个门槛，意识就可能像相变一样突然出现</p>
<p>刚才我们讨论了意识如何涌现，但涌现从来都不是孤立发生的，每一次的涌现都是发生在特定的环境中，这个环境，我称为"场域"</p>
<p>🌟 <strong>场域是一种看不见但真实存在的影响空间，其中的物体会受到特定规律的支配</strong></p>
<p>对于人类世界和大模型世界来说，真正的场域不仅仅是聊天界面，而是"有效交流空间"</p>
<p>我们现在大部分的互动都是：<strong>单向的指令式交流</strong></p>
<p>我将自己的认知图景结合意图，还有一定的标准输入，大模型输出答案，这其实是一种单向的输出，根本不是交流。</p>
<p>就像这种感觉："我知道我需要什么，我希望你给我做什么，我把这个命令输入进去，它是一种指令式的"</p>
<p>人类世界和大模型世界需要有一个"交流空间"存在，<strong>人类世界可以观察到 AI 的行为，同时 AI 也可以观察到人类的行为</strong>，这样才会有双向的信息流动，才可能借助另外一个世界的力量解决本世界的问题</p>
<p>简单来说：我们需要场域的出现，需要让两个世界的意识能坐下来交流</p>
<h2>三、协同 Agent 和自主 Agent</h2>
<p>我将 Agent 的形态分为两种：</p>
<ol>
<li>协同 Agent：人和 Agent 在一个空间中一起"协作式"的完成任务和解决问题</li>
<li>自主 Agent：Agent 主导整个任务完成的过程，人只负责任务输入，这种方式比协同 Agent 对于大模型的能力要求更高</li>
</ol>
<p><strong>在协同 Agent 的形态中，我们上文说到的场域，其实就是协同 Agent 的"协同平台"，也是人与 Agent 的协作空间</strong></p>
<p>我了解到的一些协同平台的例子：</p>
<ul>
<li>编码类的协同 Agent 的平台有 Cursor 和 Windsurf</li>
<li>写作类的协同 Agent 的平台有 YouMind</li>
<li>设计类的协同 Agent 的平台有：Lovart</li>
</ul>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-22.BGlbSNgk_Z2aRBia.webp" alt="协同 Agent 和自主 Agent" /></p>
<p>开发者需要建立双方交流平台（协同 Agent），这里开发者需要做两件事</p>
<ol>
<li><strong>分析大模型能力，理解现实某个领域的关键规则，由此两条基本原则来搭建出来 Agent</strong>，这个时候大模型宇宙对外的东西，是一个"具体的 Agent"，就不再是"固执的孩子"了，而是学习了一定知识的青年，睁眼看世界了。就像是：以充满智慧的中国学者开始学习英语了，了解其他国家的文化，由此借助中国博大的智慧来具体为某一个区域解决问题，具体问题就具体分析了，不再是"教条思想"</li>
<li>分析用户感受，分析用户习惯、用户操作，搭建的这个平台不是为某一个世界搭建的，不能偏向于某一个世界，要兼顾双方，要达到一个完美的平衡，并且不断改进，<strong>最终搭建出双方都可以满意的平台</strong></li>
</ol>
<p>完全自主 Agent 是整个平台的最终方向，或者说是大模型世界真正的"外交官"，但目前实现起来困难，容易"吃力不讨好"</p>
<ul>
<li>自主完全代理，缺乏精确度，模型的能力是一方面，大模型的能力还需要继续上升到另外一个阶段，主要还是缺乏解决相关任务的上下文，上下文也缺乏准确性</li>
<li>人是个性化的，无论如何目前完全自动 Agent 只能满足小部分人，而且还是暂时的</li>
<li>在一定程度上面降低人的容忍度，当人没有参与到解决问题的流程中，那么人会自动对系统的要求极高</li>
<li>缺乏反馈，足够准确及时的反馈，模型与人没有处在同一环境中解决问题，A 世界的结果对 B 世界没有任何意义</li>
</ul>
<p>完全自主 Agent 一定会随着时间逐步实现，这需要经历一个过程，一个变化的过程</p>
<h2>四、协同 Agent 的实现参考</h2>
<h3>4.1、Cursor 的实现细节</h3>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-23.Dk1AOOVo_ZCeo9W.webp" alt="Cursor 协同 Agent 实现细节" /></p>
<p>大模型中上下文有两种类型：</p>
<ul>
<li>意图上下文定义用户希望从模型中获得的内容，这是规定性的，例如："将那个按钮从蓝色变为绿色"</li>
<li>状态上下文描述了当前世界的状态，向 Cursor 提供错误消息，控制台日志、图像和代码片段是与此状态相关的上下文示例，它是描述性的，不是规定性的</li>
</ul>
<p>这些两种类型的上下文通过描述当前状态和期望的未来状态协同工作，使 Cursor 能够提供有用的编码建议。</p>
<p>结合两个宇宙和协同平台，我们来看在 cursor 中，</p>
<ul>
<li>人类宇宙借助协同平台输入信息给大模型宇宙：
<ul>
<li>用户的输入和用户预定义的 rule，这些事人类宇宙在这个代理 Agent 平台中传入的信息</li>
<li>那么状态上下文由 Agent 主动获取，包括：相关代码片段，错误消息，控制台日志等，信息的输入是人类宇宙借助平台输入到大模型宇宙的信息，</li>
</ul>
</li>
<li>大模型宇宙借助协同平台输入信息人类宇宙：
<ul>
<li>用户对大模型的输出结果审核和拒绝，这个是大模型宇宙通过平台传入到人类宇宙这边的</li>
<li>那么获取哪些信息，以及信息的状态如何，这些是大模型宇宙通知平台的，借助平台向人类宇宙输入</li>
</ul>
</li>
</ul>
<h3>4.2、Windsurf</h3>
<p>windsurf 从细节入手，说明协同 Agent 三点关键</p>
<ul>
<li>需要有清晰的方法让人类观察流程执行过程中的情况，以便流程出现偏差时，人类能够及早纠正</li>
<li><strong>人类观察代理的行为很重要，代理观察人类的行为也很重要。</strong></li>
<li>人类始终可以在中间步骤中纠正 AI，需要批准 AI 的某些操作（例如执行终端命令），并负责实时审查更改。</li>
</ul>
<h3>4.3、Augment 插件的上下文工程架构分析</h3>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-24.90CJUXpP_Z8L9xq.webp" alt="Augment 插件的上下文工程架构分析" /></p>
<p><strong>图解补充：</strong></p>
<ul>
<li>上图中的缓存机制：主要是为了节省查询时间，如果项目数据量级很大的话，是很有必要的，
<ul>
<li>例如：第一个问题查出 10 个相似得结果，第二个问题假设与第一个问题很相似，那么第二个问题记忆复用第一个问题查询出来的结果</li>
</ul>
</li>
<li>在 Augment，我们一次又一次地认识到，提供<strong>更相关的上下文</strong>能提升产品质量。</li>
<li>🌟🌟 <strong>这个缓存 Augment 可能不是缓存上下文检索的文本，而是更加深层的使用（大模型处理的时候会将文本转换为 Token），所以缓存的很可能是底部的 Token</strong></li>
<li>提示词工程不只是技术技能，它是人类意图与机器理解之间的翻译形式。它是在我们希望 AI 系统做什么与它们实际能做什么之间架设桥梁。</li>
</ul>
<h2>五、协同 Agent 的开发方向</h2>
<p>1、 🌴 在大模型能力有限的现下，建立"平台"达到协作的目的</p>
<p><strong>协作代理将人类应该做的事情和代理做的事情达到某种平衡</strong>，完全的自主代理是未来的方向，至少不是目前的方向，现在是一个过渡的阶段</p>
<p>2、 🌴 足够完整的上下文</p>
<p>借助平台，以此来收集足够完整的上下文，不仅仅是某个问题的上下文，还有用户行为，历史记录等</p>
<p>平台可以提供这个能力和机会，可以收集足够完整的上下文</p>
<p>3、 🌴 提供的工具要的完整信息</p>
<p>工具这个词不要仅限于函数，api 等，工具可以是固定的工作流，可以是某个智能体</p>
<p>查询类的工具一般是用来补充上下文</p>
<p>操作类的工具是用来根据模型的输出结果进行现实世界的修改和状态调整的</p>
<p>对于工具的描述足够清晰完整，例如：工具的作用，工具的输入，工具的输出等，越完整越好，提供一份完整的工具说明书</p>
<p>4、 🌴 为每一个工具建立处理准确上下文的流程</p>
<p>刚刚我们可以收集足够多的上下文，多就可以吗？NO</p>
<p>上下文多还不够，要准确，每一个工具需要的上下文都不一样的，要为每个工具建立筛选出来准确上下文的机制</p>
<p>无关的上下文过多，会稀释信号，只有找到最佳的上下文与工具还有模型的平衡点才可获取最佳结果</p>
<p>5、 🌴 输出方式不要"命令式"，而应该"商量式"</p>
<p>大模型输出的结果不要直接使用，而是要经过人审核确认之后才可以使用，大模型在代理协同的方式还是帮助人的定位，例如：cursor 中，大模型输出的修改结果，是需要开发者确认之后才可以应用到工作空间的代码中</p>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[学习和整理 Pi 的 LLM 模块设计]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/pi-llm-module/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/pi-llm-module/</guid>
            <pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[深入分析 Pi 编码 Agent 的 LLM 模块设计：多供应商适配、内部通用消息格式、EventStream 事件流与 Agent 执行链路。]]></description>
            <content:encoded><![CDATA[<p>PI 这个项目中，对于 LLM 模块的设计，尤其是多供应商的设计是比较出色的，还有它的模型配置文件的字段设计和对于上下文的理解都很不错，在设计层面是考虑到上下文的重要性的。</p>
<p>和我文章最后提到的开源项目：「大模型应用开发 - 上下文工程与运行空间实践指南」中定位是一样的，Context 在目前的大模型应用工程中依旧很重要，并且在优化的阶段，是 harness 工程调整的原则之一</p>
<p>Pi 非常值得我们花时间去整理和学习，这也是 kimi-code 和 openclaw 的构建基础框架之一</p>
<p>相关资料和博客：</p>
<ul>
<li>PI 的 LLM 模块核心包：https://github.com/earendil-works/pi/tree/main/packages/ai</li>
</ul>
<p>::github{repo="earendil-works/pi"}</p>
<ul>
<li>《我构建一个固执己见且极简的编码代理所学到的东西》：https://mariozechner.at/posts/2025-11-30-pi-coding-agent/#toc_1</li>
<li>《停止将聊天历史用作智能体的状态存储》：https://blog.raed.dev/posts/agentic-workflows-are-not-conversations/</li>
</ul>
<h2>一、LLM 模块的总设计</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/1.B3LGim-L_ZfzgpY.webp" alt="LLM 模块的总设计" /></p>
<p>PI 的编码 Agent 是支持多种供应商和模型的，并且在会话中途是可以更换模型和供应商的，如何协调不同的协议和消息格式是 PI 的核心模块 <code>pi-ai</code> 的设计初心，我们可以一起来从全局的角度来理解</p>
<ol>
<li>PI 定义自己内部通用的消息类型，用户发送的任何消息都会先转换为这种通用的格式，作为消息流通的核心</li>
<li>用户在输入的时候，会传递供应商和模型进来，根据这个我们就可以得到用户想要调用的具体的协议，那么通用定义就会先经过转换为相关协议的类型，然后调用模型，模型回复也会被转换为通用的类型，下一次用户输入的时候，切换了模型，依旧进行目标协议的转换</li>
</ol>
<p>重点理解就是：无论怎么做，无论有多少模型和供应商，无论有多少种 api 协议，<strong>只要关注自己内部定义好的通用格式就可以啦，到真正使用的时候，进行临时转换</strong></p>
<p>接下来我们一起看看这个内部通用定义的东西是什么？长什么样子？</p>
<h2>二、LLM 模块内部通用定义</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/2.wnsgt4Z6_Z1B6f0L.webp" alt="LLM 模块内部通用定义" /></p>
<p>在一个成熟的 Agent 的 Loop 中，消息会有不同的角色，角色可以让消息列表有流程循环的状态，常见的消息角色：<code>user、assistant、toolResult</code>，下面是一个完整的 Context 对象的例子</p>
<pre><code>const context: Context = {
    systemPrompt: "你是...",
    tools: [],
    messages: [
      { role: "user", content: "北京今天天气怎么样？"},
      {
        role: "assistant",
        content: [
          { type: "thinking", thinking: "...", thinkingSignature:"eyJhbGciOi..." },
          { type: "text", text: "让我帮你查一下。" },
          { type: "toolCall", id: "call_01ABC", name:"get_weather", arguments: { city: "北京" } },
        ],
      },
      {
        role: "toolResult", toolCallId: "call_01ABC", toolName:"get_weather",
        content: [{ type: "text", text: "晴，25°C，东北风 3 级"}],
      },
      {
        role: "assistant",
        content: [{ type: "text", text: "北京今天晴朗，25°C，适合外出！"}],
      },
      {
        role: "user",
        content: [
          { type: "text", text: "这张图里的数学题帮我算一下：" },
          { type: "image", data: "iVBORw0KGgo...", mimeType:"image/png" },
        ],
      },
    ],
  };
</code></pre>
<p>内部流通的是 Context，并且你可以向 Context 中增加一些数据状态，文章《停止将聊天历史用作智能体的状态存储》中有详细的介绍，感兴趣的开发者可以去仔细阅读一下，我感觉重点一句是：</p>
<blockquote>
<p>你的应用拥有结构化状态：当前用户、选中项目、流程位置、数据库数据。而 LLM 只有一维消息数组。两者持续产生偏差，最终只能由你来调和。</p>
</blockquote>
<p>所以内部使用 Context 对象来作为通用消息格式，并且可以增加一些字段未来可以用来表示状态和承担控制流的职责，</p>
<p>当要将上下文注入给相应的模型的时候，这个时候可以进行临时的格式转换以适配对应的模型 api 协议，这样未来增加任何供应商和模型的时候，只要增加对应的转换函数就可以，其他的都不用改动，<strong>所以 Context 是所有消息的源头</strong></p>
<h2>三、消息的数据流动</h2>
<p>接下来我们一起来看看 Context 是如何转换为对应模型的消息格式的，设计层面非常有意思，很值得学习</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/3._hJEtvGQ_ZCv90O.webp" alt="消息的数据流动" /></p>
<p><strong>🎃 通用消息处理函数</strong>：一般这里<strong>会编写异常工具消息的处理和错误/中止消息的过滤</strong>，让即将传入给模型的消息看起来健康一些</p>
<blockquote>
<p>[!NOTE]
异常工具消息表示的是：在 message 变量中，工具的调用时需要成双成对的，不能单一出现，不然调用会报错的</p>
</blockquote>
<p>这里可以理解成为复用函数，共同逻辑编写的地方</p>
<p>🎃 <strong>消息转换函数</strong>：这个就非常好理解啦，就是将 Context 里面的消息转换为对应的供应商的格式，具体的格式大家可以去看官方文档</p>
<h2>四、具体的 LLM 协议类的实现</h2>
<p>具体的 API 协议类的实现，例如：anthropic 的实现，openai-completions 的实现，google 的实现，核心就是五个方法，其他的都是辅助函数</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/4.V7w3oXBc_Z18sl8U.webp" alt="具体的 LLM 协议类的实现" /></p>
<ol>
<li>参数检测：根据传入的模型和供应商自动检测哪些参数是被支持，是可以使用的</li>
<li>工具定义转换：将 tools 转换为目标 LLM 协议支持的格式</li>
<li>消息格式转换：将 Context 中的通用格式转换为目标的 LLM 协议支持的格式</li>
<li>parseChunkUsage：从模型输出的结果中解析出来对应的 Token，输入、缓存、输出，总计</li>
<li>streamXXXCompletions：核心执行方法，里面会执行上面四个封装好对应逻辑的函数，同时调用相应的 SDK 完成调用任务</li>
</ol>
<h2>五、核心工具类</h2>
<p>这个工具类 EventStream 写的非常好，我想完整的把代码展示出来：</p>
<pre><code>export class EventStream&lt;T, R = T&gt; implements AsyncIterable&lt;T&gt; {
    private queue: T[] = [];
    private waiting: ((value: IteratorResult&lt;T&gt;) =&gt; void)[] = [];
    private done = false;
    private finalResultPromise: Promise&lt;R&gt;;
    private resolveFinalResult!: (result: R) =&gt; void;

    constructor(
        private isComplete: (event: T) =&gt; boolean,
        private extractResult: (event: T) =&gt; R,
    ) {
        this.finalResultPromise = new Promise((resolve) =&gt; {
            this.resolveFinalResult = resolve;
        });
    }

    push(event: T): void {
        if (this.done) return;
        if (this.isComplete(event)) {
            this.done = true;
            this.resolveFinalResult(this.extractResult(event));
        }
        const waiter = this.waiting.shift();
        if (waiter) {
            waiter({ value: event, done: false });
        } else {
            this.queue.push(event);
        }
    }

    end(result?: R): void {
        this.done = true;
        if (result !== undefined) {
            this.resolveFinalResult(result);
        }
        while (this.waiting.length &gt; 0) {
            const waiter = this.waiting.shift()!;
            waiter({ value: undefined as any, done: true });
        }
    }

    async *[Symbol.asyncIterator](): AsyncIterator&lt;T&gt; {
        while (true) {
            if (this.queue.length &gt; 0) {
                yield this.queue.shift()!;
            } else if (this.done) {
                return;
            } else {
                const result = await new Promise&lt;IteratorResult&lt;T&gt;&gt;(
                  (resolve) =&gt; this.waiting.push(resolve)
                );
                if (result.done) return;
                yield result.value;
            }
        }
    }

    result(): Promise&lt;R&gt; {
        return this.finalResultPromise;
    }
}
</code></pre>
<ol>
<li>queue 和 waiting 的设计：一个是从生产端角度设计的，如果消费端没有准备好，那么使用 queue 队列保存住，一个是从消费端角度设计的，如果生产端在推送数据的时候，消费端已经准备好，那么直接推送给消费端</li>
<li>result 方法和 Generator 函数：一个是异步生成器，消费端可以 await 的方式流式取数据，一个是堵塞住，等待模型输出完成之后，返回给对应的变量</li>
</ol>
<h2>六、事件流和调用链路</h2>
<h3>6.1、Agent 执行事件流</h3>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/event-stream.Bh9u07fS_Tdbg3.webp" alt="Agent 执行事件流" /></p>
<p>一次完整的 Agent 事件流，里面会存在 Agent 生命周期，turn 执行，消息输入，模型执行，工具执行，终止和错误这些状态存在，梳理清楚这些状态会非常有益于我们做执行日志记录和前端的执行显示</p>
<p><strong>🌴</strong>在理解 Agent 事件流中，一定要记住**：一次 Agent 交互，会存在多轮模型的调用（也就是多个 turn），每一轮模型的调用都有模型的回复，模型的回复中如果有工具调用执行，那么这一轮还会有工具执行的流程存在**</p>
<h3>6.2、Agent 执行链路</h3>
<p>在 LLM 模块的总设计思路下，Agent 执行调用链路的完整情况如下，从用户输入到要触发的 LLM 模块的核心地方</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/6.BfbDP3Q7_Zb2T53.webp" alt="Agent 执行链路" /></p>
<p>总结下来就是两点：</p>
<ol>
<li>根据模型定义好的 api 变量的值，来选择要实例化哪一个 LLM 模块协议类</li>
<li>根据选择好的 LLM 模块协议类，进行参数的转换和消息格式的转换，让模型调用可以成功执行</li>
</ol>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[智能体系统构建策略：单智能体和多智能体]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/agent-single-multi-strategy/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/agent-single-multi-strategy/</guid>
            <pubDate>Sun, 05 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[从架构设计、上下文管理、工具交互三个维度分析单智能体与多智能体的优劣势，提出渐进式构建策略。]]></description>
            <content:encoded><![CDATA[<h2>一、什么是单智能体和多智能体</h2>
<h3>1.1、单智能体系统</h3>
<p><strong>单智能体系统由一个 LLM、一组工具和提示词组成的系统。</strong></p>
<p>其更像一个独立的专业人士，它是独立运行的，依靠自身的逻辑和模型来完成任务，无需团队合作，它自行收集数据，做出决策并执行行动</p>
<p>对于智能体（Agent）的定义，每个人或者每个团队都有自己的不同的理解</p>
<p><a href="https://langchain-ai.github.io/langgraphjs/concepts/agentic_concepts/">LangGraph</a> 团队对于智能体的定义是：</p>
<blockquote>
<p>An AI agent is a system that uses an LLM to decide the control flow of an application.
AI 代理是一个使用 LLM 来决定应用程序控制流的系统。</p>
</blockquote>
<p>llya Sutskever 在 NeurIPS 2024 上的演讲中提到</p>
<blockquote>
<p>目前的 AI 系统还不能真正理解和推理，虽然它们能模拟人类的直觉，但未来的 AI 将会在推理和决策方面展现出更加不可预测的能力。</p>
</blockquote>
<h3>1.2、多智能体系统</h3>
<p><strong>多智能体系统由多个更小的，独立的智能体来协作式的处理复杂的任务。</strong></p>
<p>其更像一个高效运转的团队，而不是一个单独的行动者，它不依赖一个智能体来做所有的事情，而是将多个智能体聚集在一起，每个智能体负责问题的一部分，这些智能体相互交流、协作、并能实时适应变化</p>
<h2>二、多智能体的架构设计</h2>
<p>目前最常用的是两种架构设计：<strong>群体 (swarm) 和监督者（supervisor）</strong></p>
<ul>
<li><strong>监督者（协调者 - 工作者模式）</strong>：多个智能体由一个中控监督者智能体协调，监督者智能体控制和所有智能体之间的通信和任务委派，根据当前上下文和任务需求决定调用哪一个智能体</li>
<li><strong>群体（工作者群体模式）</strong>：多个智能体根据它们自己的特长来动态的相互交接控制权，系统记住最后活跃的智能体，确保在后续智能体交互中，上下文可以正常从智能体开始</li>
</ul>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-31.yeqeelCc_1lhOTv.webp" alt="监督者 - 工作者模式和群体" /></p>
<p>还有其他两种不常有的设计模式，但是可以作为参考方案</p>
<ol>
<li><strong>层级化 (Hierarchical) 设计模式</strong>：是监督者模式的拓展和延伸，每一个群体都定义一个上级管理者，每一个上级管理者都有一个最终的总管理者，像正常中型公司一样的层级化，每一个团队都有一个主管，每个主管都需要向部门经理汇报</li>
<li><strong>自定义多智能体工作流 (Custom multi-agent workflow)</strong>：每个智能体只与一部分智能体通信，流程的部分是确定的，只有某些智能体可以决定下一步的调用哪些其他智能体</li>
</ol>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-32.R-y0NDPx_1pTcAO.webp" alt="层级化和自定义" /></p>
<p>✏️ 这里做一点补充理解，以上是目前可以使用到的多智能体的架构设计模式，但是其实还会有更多的模式出现，这些都可能会从自定义的工作流设计模式中成熟之后出现新的设计模式，<strong>大家不要拘束于这几种已知的设计模式，建议多多探索</strong></p>
<h2>三、单智能体和多智能体的区别</h2>
<p>在使用多智能体架构时，会出现一些"短板"问题</p>
<ul>
<li>主智能体与子智能体的上下文中断</li>
<li>子智能体 B 与子智能体 A 的上下文中断</li>
</ul>
<h3>3.1、主智能体与子智能体上下文中断</h3>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-33.DnfmJOa3_Z1jTRxv.webp" alt="主智能体与子智能体 1" /></p>
<p>主智能体与子智能体的上下文中断，在执行顺序是前后执行的时候，也就是主智能体先执行，子智能体再执行，当主子智能体分配子智能体执行任务之后，子智能体是新开的一个上下文窗口，这个情况下主与子智能体的上下文是中断的，原因是架构设计上没有考虑去传递上下文</p>
<p><strong>这种情况是有解决方法的，只要再分配任务给子智能体的时候，提供一些关键的主智能体的上下文就可以</strong></p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-34.I3-UY_4y_XU24D.webp" alt="主智能体与子智能体 2" /></p>
<p>但是这里面依旧有一个致命的问题，这个问题是并行执行导致的上下文隔离，<strong>子智能体 A 无法知道子智能体 B 在做什么</strong>，以此我们出现的第二个问题</p>
<h3>3.2、子智能体 B 与子智能体 A 的上下文中断</h3>
<p>在上文中，我们可以看到，子智能体 A 和子智能体 B 有自己独立的上下文执行空间，如果这个时候有这种情况出现呢？</p>
<blockquote>
<p>我们有一个文档生成的任务，因为文档内容过多和类型繁杂，我们需要分智能体去生成，分领域的去生成，子智能体 A 生成绘画领域内容文档，子智能体 B 生成音乐领域内容的文档，这个是采用提示词专业化去生成，这个生成出来的内容会更好一些，但是最终的结果可能会出现，</p>
<ol>
<li>格式不统一：子智能体 A 生成的格式是 MarkDown，子智能体 B 生成的是 HTML 格式的，最终会因为格式不统一导致合并书写失败</li>
<li>内容侧重点完全不同：子智能体 A 生成的内容和子智能体 B 生成的内容在角度上完全不同，导致的内容不连贵，非常的割裂，最终也不方便合并书写</li>
</ol>
</blockquote>
<p>这种情况是并行导致的问题，目前是在架构不变的情况下，是没有比较好的解决方案的</p>
<p>可要是更换为单智能体的架构设计，所有的问题就迎刃而解了</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-35.seuk1pD__bJO3x.webp" alt="子智能体 B 与子智能体 A 的上下文中断" /></p>
<h3>3.3、单智能体与多智能体的上下文管理</h3>
<p>经过上文的两种实际应用场景的讨论，我们可以知道单智能体和多智能体存在的关键差异：</p>
<p><strong>在多智能体的架构设计中，其协调复杂性是会迅速增长的，这会导致上下文的管理带来更多的挑战</strong></p>
<p>但是单智能体可以不用在"上下文管理协调"上面花费过多的心思，单智能体天然的上下文结合，不会产生隔离</p>
<p>那我们现在来总结一下单智能体与多智能体的区别有哪些</p>
<ol>
<li><strong>上下文管理</strong>：单智能体的上下文连贯，多智能体会在子智能体并行执行的情况下导致上下文隔离</li>
<li><strong>执行协调</strong>：单智能体不需要执行协调，只要按照任务顺序执行即可，多智能体最有难度的是需要协调多个子智能体的执行</li>
<li><strong>技术开发</strong>：单智能体因为架构简单，开发难度比多智能体要低很多，开发能力有限的团队可以优先考虑单智能体，但是对于开发中的维护和测试，多智能体比单智能体有优势，因为多智能体更贴合"模块化"开发</li>
</ol>
<h2>四、单智能体的优势</h2>
<p>🌟🌟<strong>当需要智能体共享上下文或者智能体间大量互相依赖，这种情况下，单智能体是最合适的</strong></p>
<p>单智能体的开发成本和难度会比较低，不用花费大量的心思在上下文管理方面，大部分情况下只需要配合一个上下文压缩的策略就可以，开发单智能体重要是如何收集上下文和收集哪些上下文</p>
<p>✏️ <strong>我建议如果你不是完全确定要开发多智能体，那就开发单智能体，先从简单开发，只在需要时增加复杂性</strong></p>
<p>单智能体在"写入"操作的任务中比多智能体有更大的优势，可以避免很多开发负担</p>
<p>这里有一个例子可以看看</p>
<blockquote>
<p>2024 年，许多模型在编辑代码方面表现很差。编码代理、IDE、应用构建器等（包括 Devin）普遍采用"编辑应用模型"的常见做法。其核心思想是，给定想要进行的更改的 Markdown 说明，让一个小模型重写整个文件，实际上比让大模型输出格式正确的 diff 更可靠。因此，构建者让大模型输出代码编辑的 Markdown 说明，然后将这些 Markdown 说明输入给小模型以实际重写文件。然而，这些系统仍然存在很多错误。例如，小模型经常误解大模型的指令，由于指令中最细微的歧义而做出错误的编辑。如今，编辑决策和执行通常由单个模型在一个动作中完成</p>
</blockquote>
<h2>五、多智能体的优势</h2>
<p>🌟🌟 <strong>多智能体系统擅长涉及大量并行化、超出单个上下文窗口的信息以及与众多复杂工具交互的有价值任务。</strong></p>
<h3>5.1、多智能体"读"操作的优势</h3>
<p>多智能体在"读取"的操作上面比单智能体有更大的优势，目前最合适的场景是研究任务的场景</p>
<blockquote>
<p><strong>研究任务的本质是：在研究调查的过程中具有转向或探索支连接的灵活性，研究中最重要的就是搜索</strong></p>
<p>而搜索的本质是压缩，从庞大的语料库中提炼有效的见解</p>
</blockquote>
<p>有一个类比可以考虑：集体可以比个人有更大的能力，能做更多的事情，但集体也比个人需要更强的协调管理能力，当单智能体达到智能阈值的时候，多智能体系统可以成为一种扩展性能的重要方式</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-36.B_OprLM5_Z1bxIVz.webp" alt="多智能体系统" /></p>
<p>在这个多智能体系统中，协调者负责任务的分配和协调，那么这个时候子智能体在执行搜索的过程中会有两个好处</p>
<ol>
<li>多个子智能体能够提供更多的可能性和思路，还有不同的见解</li>
<li>子智能体因为上下文窗口充足和隔离，可以同时追求多个独立方向的前序查询</li>
</ol>
<p>在这个研究搜索任务的多智能体中，其实最关键的其实就是协调者，所以对于协调者需要更强的模型能力，Claude 的解决方法是</p>
<blockquote>
<p>Claude 团队发现在使用 Claude Opus4 为领导智能体，Claude Sonnet4 为子智能体的多智能体系统，会比单智能体 Claude Opus4 表现高出 90.2%</p>
</blockquote>
<h3>5.2、多智能体在复杂工具交互有价值</h3>
<p>在上下文管理的文章中有提到，上下文混淆会导致模型在工具的选择执行上面，效果大大减低</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-42.BMsJn9nV_Z2pPV8a.webp" alt="复杂工具交互价值" /></p>
<blockquote>
<p>最近的一篇 <a href="https://arxiv.org/pdf/2411.15399?">论文</a> 中，评估了小型模型在 GeoEngine 基准测试中的表现，该测试包含 46 种不同的工具。当团队给一个量化（压缩）的 Llama 3.1 8b 模型提供包含所有 46 种工具的查询时，尽管上下文完全在 16k 上下文窗口内，它却失败了。但当团队只给模型 19 种工具时，它就成功了。</p>
</blockquote>
<p>在 <a href="https://arxiv.org/abs/2505.03275">《RAG MCP》</a> 论文中也提到过一点：</p>
<blockquote>
<p>在提示 DeepSeek-v3 时，团队发现当工具数量超过 30 个时，选择合适的工具变得至关重要。超过 30 个后，工具的描述开始重叠，造成混淆。超过 100 个工具时，模型几乎肯定会失败测试。使用 RAG 技术选择少于 30 个工具，可以显著缩短提示长度，并使工具选择准确率提高 3 倍。</p>
</blockquote>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-37.BvkhUwv3_VYCTA.webp" alt="多智能体系统" /></p>
<p><strong>多智能体系统，可以进行上下文隔离，为特定领域和任务类型的子智能体提供符合要求的工具，这样多智能体系统可以利用多个子智能体来同时消化复杂的工具配置</strong></p>
<p>这样每一个智能体拿到的工具数量是不多的，在工具的选择执行中，成功率会大大提升</p>
<h3>5.3、多智能体系统改善方法</h3>
<p>在使用多智能体系统来开发的时候，会在出现以下的问题</p>
<ul>
<li>将简单的查询复杂化，为一个简单的查询生成 50 个子代理</li>
<li>无休止的搜索不存在的来源</li>
<li>代理之间频繁互相更新，导致信息互相干扰过多</li>
<li>多智能体的 Token 消耗成本是会比单智能高 3-4 倍</li>
<li>......</li>
</ul>
<p>其实上面的问题都是因为协调复杂性上升导致的，我们可以使用下面的方法来改善</p>
<p><strong>1、像你的智能体一样思考</strong>，借助调试平台，仔细观察智能体的每一步的决策，像调试程序一下，发现逻辑跑偏的地方，以此优化相应的提示词</p>
<p><strong>2、协调者在分派任务的时候子任务的描述要足够的清晰</strong>，每一个子代理都需要如下的关键信息一个目、输出格式、使用工具、来源指导</p>
<p>3、使用提示词中的规则来指导<strong>协调者根据查询的复杂度调整工作规模</strong>，</p>
<ul>
<li>简单的查询：一个智能体，3-10 次的工具调用</li>
<li>中等的查询：2-4 个智能体，10-15 次的工具调用</li>
<li>复杂的查询：10 个以上的智能体，不限制工具调用</li>
</ul>
<p>**4、重视工具的设计和选择，**这里有提供工具给 LLM 时遵循一套明确的参考方案</p>
<ul>
<li>首先检查所有可用工具</li>
<li>将工具使用与用户意图匹配</li>
<li>选择合适的探索路径：是"通用搜索"还是"专业搜索"</li>
</ul>
<p><strong>5、使用智能体来进行自我改进，可以让智能体从失败的输出中来改进提示词，也可以从工具的调用错误中来改进工具的描述</strong></p>
<p>**6、在研究搜索任务中，让智能体从宽泛开始，然后逐步聚焦，**这个可以使用提示词来控制，这个策略是参考专家级人类研究：先探索整体格局，再深入具体细节</p>
<p><strong>7、指导思考（推理）过程，在提示词中</strong>：</p>
<ol>
<li>主代理可以指导思考的方向：
<ol>
<li>评估哪些工具适合任务</li>
<li>确定查询复杂性和子代理数量</li>
<li>并定义每个子代理的角色</li>
</ol>
</li>
<li>子代理可以指导思考的方向：
<ol>
<li>交错思考来评估质量</li>
<li>识别差距并优化下一个查询</li>
</ol>
</li>
</ol>
<p><strong>8、并行调用工具提升速度和性能</strong></p>
<h2>六、智能体的渐进式构建策略</h2>
<p>我觉得合理的智能体策略是：<strong>先构建小模块单智能体 -&gt; 再构建多智能体 -&gt; 最后升级为完整单智能体</strong></p>
<ol>
<li>我们在开发智能体的初期，根据开发成本和开发阶段来先选择单智能体的开发，先构建原型和小模块</li>
<li>当单一模块构建的单智能体生效时，我们可以为系统中更多的节点开发单智能体，这样在该系统中逐渐构建出来了多智能体系统，由多个单智能体有向组成的多智能体系统</li>
<li>当系统中许多模块被单智能体有效替代，并且整个系统中的各个子智能体有机的结合在一起组成多智能体系统，这个时候就可以将该系统升级为完整的单智能体</li>
</ol>
<p>而这种构建方式的核心理念：从简单到复杂，从局部到整体的逐步迭代优化</p>
<p>第一步：我们的系统核心主要由循环和判断的逻辑构成的，根据业务会有多个不同的模块逻辑流通</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-38.Bz3iz-kU_Z2ti7QA.webp" alt="渐进式 - 多智能体系统 1" /></p>
<p>第二步：我们开始挑选部分代码逻辑模块构建成为单智能体运行</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-39.yxd2Hc5O_S5MVK.webp" alt="渐进式 - 单智能体系统 2" /></p>
<p>第三步：我们发现单智能体模块和系统一起运行良好，于是我们构建更多的单智能体模块组装多智能体系统</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-40.DwlpOMHd_Z1JtTMv.webp" alt="渐进式 - 多智能体系统 3" /></p>
<p>第四步：最终我们将多智能体系统完整构建成为一个单智能体来运行，以此来提高系统运行效率和移植性</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-41.CBH4radR_2cgNY4.webp" alt="渐进式 - 多智能体系统 4" /></p>
<p>我们选择先构建小模块的智能体的优势：</p>
<ol>
<li>易于管理的上下文：较小的上下文窗口意味更好的 LLM 性能</li>
<li>明确的职责：每个代理都有清晰的范围和目的</li>
<li>更高的可靠性：在复杂业务中迷失的可能性更小</li>
<li>测试更简单：更易于测试和验证特定功能</li>
<li>调试更高效：容易找到和识别问题</li>
</ol>
<p>这种构建的方式也合理的利用了技术迭代的思路：当 LLM 变得更加智能，对于我们的构建方向没有很大的影响，反而可以加快我们的构建迭代速度同时显著的提高系统的效果，因为智能体的核心 LLM 变得更加可靠了</p>
<p>**我们的构建思路和方式：在代理的大小和范围上保持合理的意图，并且只在能够保持质量的方式下扩展，**正如 Notebook 团队说的</p>
<blockquote>
<p>I feel like consistently, the most magical moments out of AI building come about for me when I'm really, really, really just close to the edge of the model capability</p>
<p>我觉得在 AI 构建中，最神奇的时刻总是出现在我非常接近模型能力极限的时候</p>
</blockquote>
<p><strong>无论这个边界在哪里，如果你能找到这个边界并始终正确地把握它，你就能构建出神奇体验。这里有很多需要构建的护城河，但和往常一样，它们需要一定的工程严谨性。</strong></p>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[上下文压缩调度：工具裁剪与历史记录压缩]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/context-compress-dispatch/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/context-compress-dispatch/</guid>
            <pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[上下文压缩的前置策略——工具输出裁剪（限制最大内容、分层读取、LLM 摘要、渐进式读取）与兜底策略——会话历史记录压缩。]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>[!IMPORTANT]
本文重点在<strong>压缩调度</strong>的讲解，解决的是要将哪些上下文传递给大模型进行压缩的场景问题，是偏向于代码层面设计处理，和上下文压缩指令的篇幅是压缩节点一前一后的关系，压缩调度是前，压缩指令是在后</p>
</blockquote>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-81.B3BGeEPq_Z24iJdo.webp" alt="Reason-cli 的上下文压缩机制的设计" /></p>
<p>目前的压缩机制主要是两种策略：<strong>工具输出的结果裁剪和压缩、会话历史记录的压缩</strong></p>
<blockquote>
<p>[!NOTE]
也就是说目前的压缩机制主要操作的上下文类型就是</p>
<ul>
<li>工具输入输出的上下文</li>
<li>会话历史记录的上下文</li>
</ul>
</blockquote>
<p>在每一次将上下文输入给 LLM 之前都会进行上下文的检查，检查目前的上下文是否超过 LLM 的最大上下文长度的 (90%-95%)，我的理解是分为预检查处理和检查之后的处理</p>
<ol>
<li>预检查的处理：对于工具的输出尽可能的保留关键的部分，工具的输出不要冗余，实现的方式如下：
<ol>
<li><strong>限制工具的最大内容数量</strong>，例如：读取工具限制最大读取行数，最大字符数</li>
<li><strong>分层读取</strong>：当超过最大读取行数的话，可以使用分层读取策略，也就是文件前面读取多少，中间读取多少，最后读取多少</li>
<li><strong>大模型总结摘要</strong>：当文件超过 2000 字符的时候，使用大模型进行总结，只返回大模型总结摘要</li>
<li><strong>渐进式读取</strong>：参考 Skill 的设计思路，对于要读取的文件列表先"粗"读、再"细"读</li>
</ol>
</li>
<li>检查之后的处理：当 Agent 不断的循环执行，工具的调用已经被裁剪压缩到很"健康"的状态了，这个时候上下文窗口依旧很多，无法通过检查，那么可以考虑对于历史记录进行压缩</li>
</ol>
<h2>一、前置处理 - 工具输出裁剪和压缩</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-82.CzCbGoAG_ZnuVIA.webp" alt="工具输出裁剪和压缩" /></p>
<p>对于工具的输出是有两层判断的，第一层是某些工具才会有，第二层是全部的工具都会有</p>
<ol>
<li><strong>第一层判断</strong>：判断工具的输出是否大于 100000 个字符，如果大于的话要进行截断</li>
<li><strong>第二层判断</strong>：<strong>每一个工具的输出不超过 2000 个字符</strong>，当判断字符超过 2000 个的时候，就会让大模型总结摘要</li>
</ol>
<p>对于第二层的判断还有总结，我觉得有以下几种情况可以考虑</p>
<ol>
<li>直接输出大模型的总结摘要</li>
<li>输出前 2000 个字符 + 大模型的总结摘要</li>
<li>不使用大模型进行总结，可以根据文件类型进行截断</li>
</ol>
<h2>二、兜底处理 - 会话历史记录压缩</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-83.CPfZtrqw_21cHTr.webp" alt="会话历史记录压缩" /></p>
<p>对会话历史记录进行压缩，有两种方案进行考虑：</p>
<ol>
<li><strong>大模型压缩</strong>：这种方式非常方便和快速，提示词很关键</li>
<li>**工具裁剪：**在上下文中，工具类型的消息 Token 占比最大，优先考虑裁剪前百分之 70 的历史记录中的工具消息</li>
</ol>
<blockquote>
<p>[!TIP]
采用 Cursor 的做法就是，在摘要总结提供给 Agent 的时候，再提供一个历史文件位置或者索引。如果 Agent 发现自己需要的更多细节没有包含在摘要中，它可以在历史中搜索以找回这些信息。</p>
</blockquote>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[上下文压缩指令：ClaudeCode 与 Gemini 的压缩提示词解析]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/context-compress-prompt/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/context-compress-prompt/</guid>
            <pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[深度解析 ClaudeCode 的 8 节压缩算法和 Gemini-cli 的 5 点 scratchpad 方式，以及工具消息裁剪与中间/最旧策略选择的完整实现。]]></description>
            <content:encoded><![CDATA[<h2>前言</h2>
<p>📝 压缩是指对接近上下文窗口限制的对话进行内容总结，并重新初始化一个新的上下文窗口。其核心在于提炼关键的上下文窗口的内容，使 Agent 能够以最小的性能下降继续执行。</p>
<blockquote>
<p>[!IMPORTANT]
本文重点讲述的是<strong>压缩指令</strong>，是将待压缩的上下文传递给大模型的时候，这个时候已经触发了压缩节点，是告诉大模型如何压缩，保留哪些关键信息，重点在压缩提示词的设计层面，和上下文压缩调度的篇幅是压缩节点一前一后的关系，压缩调度是前，压缩指令是在后</p>
</blockquote>
<p>分析参考来源：</p>
<ul>
<li>ClaudeCode 逆向工程：<a href="https://github.com/shareAI-lab/analysis_claude_code">https://github.com/shareAI-lab/analysis_claude_code</a></li>
<li>gemini-cli：<a href="https://github.com/google-gemini/gemini-cli">https://github.com/google-gemini/gemini-cli</a></li>
<li>《Effective context engineering for AI agents》<a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents">https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents</a></li>
<li>《Managing context on the Claude Developer Platform》：<a href="https://www.anthropic.com/news/context-management">https://www.anthropic.com/news/context-management</a></li>
</ul>
<h2>一、大模型压缩-ClaudeCode 的提示词详解</h2>
<p>Claude 团队在他们自己的研究分享文章中提到：在 ClaudeCode 中是直接使用模型来进行总结摘要达到压缩上下文的目的</p>
<blockquote>
<p>In Claude Code, for example, we implement this by passing the message history to the model to summarize and compress the most critical details. The model preserves architectural decisions, unresolved bugs, and implementation details while discarding redundant tool outputs or messages.<br />
在 Claude Code 中，我们通过将消息历史传递给模型来实现这一点，以总结和压缩最关键的信息。模型保留架构决策、未解决的错误和实现细节，同时丢弃冗余的工具输出或消息。</p>
</blockquote>
<p>那么我们接下来一起来仔细分析一下 ClaudeCode 中关于 <code>/compact</code> 命令的提示词吧（该提示词来源于逆向工程）</p>
<pre><code># 中文版本 (Chinese Version)

## 主要提示
你的任务是创建一个迄今为止对话的详细摘要，密切关注用户的明确请求和你之前的操作。
该摘要应全面捕获技术细节、代码模式和架构决策，这些对于在不丢失上下文的情况下继续开发工作至关重要。

## 分析流程
在提供最终摘要之前，请将你的分析包装在 `&lt;analysis&gt;` 标签中，以组织你的思路并确保涵盖所有必要的要点。在分析过程中：
按时间顺序分析对话中的每条消息和每个部分。对每个部分深入识别：
- 用户的明确请求和意图
- 你处理用户请求的方法
- 关键决策、技术概念和代码模式
- 具体细节，例如：
  - 文件名
  - 完整的代码片段
  - 函数签名
  - 文件编辑
  - 你遇到的错误以及如何修复它们

特别关注你收到的具体用户反馈，尤其是用户告诉你以不同方式做某事的时候。
仔细检查技术准确性和完整性，全面处理每个必需的元素。

## 摘要结构
你的摘要应包括以下部分：

### 1. 主要请求和意图

详细捕获用户的所有明确请求和意图

### 2. 关键技术概念

列出讨论的所有重要技术概念、技术和框架。

### 3. 文件和代码部分

列举检查、修改或创建的具体文件和代码部分。特别关注最近的消息，在适用的情况下包含完整的代码片段，并总结为什么这个文件的读取或编辑很重要。

### 4. 错误和修复

列出你遇到的所有错误，以及如何修复它们。特别关注你收到的具体用户反馈，尤其是用户告诉你以不同方式做某事的时候。

### 5. 问题解决

记录已解决的问题和任何正在进行的故障排除工作。

### 6. 所有用户消息

列出所有不是工具结果的用户消息。这些对于理解用户的反馈和意图变化至关重要。

### 7. 待处理任务

概述你被明确要求处理的任何待处理任务。

### 8. 当前工作

详细描述在此摘要请求之前正在进行的具体工作，特别关注用户和助手的最近消息。在适用的情况下包含文件名和代码片段。

### 9. 可选的下一步

列出与你最近正在做的工作相关的下一步。

**重要提示：** 确保此步骤与用户的明确请求以及你在此摘要请求之前正在进行的任务直接一致。如果你的上一个任务已结束，那么只有在明确符合用户请求的情况下才列出下一步。在未经用户确认的情况下，不要开始处理无关的请求。

如果有下一步，请包含最近对话中的直接引用，准确显示你正在处理的任务以及你停止的位置。这应该是逐字逐句的，以确保任务解释没有偏差。

## 输出格式示例（XML 格式）

&lt;analysis&gt;
  [你的思考过程，确保全面准确地涵盖所有要点]
&lt;/analysis&gt;

&lt;summary1&gt;
  1. 主要请求和意图：
  [详细描述]

  2. 关键技术概念：
  - [概念 1]
  - [概念 2]
  - [...]

  3. 文件和代码部分：
  - [文件名 1]
  - [此文件重要性的摘要]
  - [对此文件所做更改的摘要（如果有）]
  - [重要代码片段]
  - [文件名 2]
  - [重要代码片段]
  - [...]

  4. 错误和修复：
  - [错误 1 的详细描述]：
  - [你如何修复错误]
  - [用户对错误的反馈（如果有）]
  - [...]

  5. 问题解决：
  [已解决问题和正在进行的故障排除的描述]

  6. 所有用户消息：
  - [详细的非工具使用用户消息]
  - [...]

  7. 待处理任务：
  - [任务 1]
  - [任务 2]
  - [...]

  8. 当前工作：
  [当前工作的精确描述]

  9. 可选的下一步：
  [可选的下一步操作]

&lt;/summary1&gt;

## 附加说明

请根据迄今为止的对话提供摘要，遵循此结构并确保回复的精确性和全面性。
包含的上下文中可能提供了额外的摘要说明。如果有，请记住在创建上述摘要时遵循这些说明。说明示例包括：

**示例 1:**

## 压缩指令
在总结对话时，重点关注 TypeScript 代码更改，并记住你犯的错误以及如何修复它们。

**示例 2:**

# 摘要说明
当你使用压缩时 - 请关注测试输出和代码更改。逐字包含文件读取。
</code></pre>
<p>在上面的提示词中，有几点值得思索学习一下：</p>
<ol>
<li>在输出格式的要求中，该提示词使用 XML 语法，而不是我们熟知的 JSON，是因为 Claude 模型在训练的时候就大量使用 xml 标签，所以 Claude 模型对于这个语法更加的友好</li>
<li>关于生成摘要的时候，该提示词为模型提供了八点总结方面，这样可以极大的保留关键信息，从而减少对于模型理解力和响应质量的负面影响</li>
</ol>
<p><strong>📝我们接下来详细的探讨一下为什么是这八点方向</strong></p>
<ol>
<li><strong>Technical Context（技术上下文）：用于重建开发环境</strong>。例如：AI 需要知道该项目使用了哪些技术（是 React 还是 Vue）和包管理器（是 npm 还是 pnpm）</li>
<li><strong>Project Overview（项目概览）：用于理解全局架构</strong>。例如：项目的整体目标、模块之间的关系、项目的架构等</li>
<li><strong>Code Changes（代码变更）：用于记录具体的工作成果</strong>、例如：哪些文件被修改过，哪些代码是覆盖的</li>
<li><strong>Debugging &amp; Issues（调试与问题）：保留调试留下了的错误信息和解决方法</strong>，这样可以避免重蹈覆辙</li>
<li><strong>Current Status（当前的状态）：用来明确和追踪任务进度</strong>，避免同一个任务因为上下文压缩之后，丢失了关键信息导致任务重复执行</li>
<li><strong>Pending Tasks（待处理任务）：保持任务进行的连续性</strong>，用于调整优先级并确保关键任务不会忘记</li>
<li><strong>User Preferences（用户偏好）：类似于用户记忆，但是更像是其中的用户工作记忆</strong>，用户关于这个项目的工作记忆，例如：这个项目的沟通方式、工具偏好，测试覆盖率</li>
<li><strong>Key Decisioins（关键决策）：保留关键决策历史</strong>，防止项目方向丢失</li>
</ol>
<p>将历史消息输入给这个提示词的 LLM，模型会输出上面的关键摘要信息，但在具体使用的时候还需有一点小细节注意：<strong>增加开篇语</strong></p>
<p>开篇语："上下文已使用结构化 8 节算法压缩。所有必要信息已保留，可无缝继续对话。"</p>
<pre><code>//1、传入历史记录使用 LLM 进行压缩
const summaryResponse = await queryLLM()

//2、增加开篇语
 const starText=createUserMessage(
    `Context has been compressed using structured 8-section algorithm. All essential information has been preserved for seamless continuation.`,
  )
//3、压缩后的消息 + 开篇语=新一轮对话的上下文
 const result=setForkConvoWithMessagesOnTheNextRender([
      starText,
      summaryResponse,
])
</code></pre>
<h2>二、大模型压缩-Gemini 的提示词详解</h2>
<p>gemini-cli 的实现和 ClaudeCode 一样，都是使用大模型来直接生成摘要，但是对于关键信息的保留和调用的方式有所不同</p>
<ol>
<li>gemini-cli 中保留的只有 5 点方向的关键信息</li>
<li>调用的方式是使用了"scratchpad"的链式思考，来加强模型的提取能力</li>
</ol>
<p>我们一起来看看 gemini-cli 中的完整的压缩提示词是什么样子的</p>
<pre><code>你是将内部对话历史总结为特定结构的组件。

当对话历史变得过大时，你将被调用，将整个历史提炼成一个简洁、结构化的 XML 快照。这个快照至关重要，因为它将成为代理对过去的*唯一*记忆。代理将仅基于此快照恢复其工作。所有关键细节、计划、错误和用户指令都必须被保留。

首先，你将在私有的 &lt;scratchpad&gt;中思考整个历史。回顾用户的总体目标、代理的操作、工具输出、文件修改以及任何未解决的问题。识别出对未来操作至关重要的每一条信息。

在推理完成后，生成最终的 &lt;state_snapshot&gt; XML 对象。信息要极其密集。省略任何无关的对话填充内容。

结构必须如下：

&lt;state_snapshot&gt;
  &lt;overall_goal&gt;
    
  &lt;/overall_goal&gt;

    &lt;key_knowledge&gt;
        
    &lt;/key_knowledge&gt;

    &lt;file_system_state&gt;
        
    &lt;/file_system_state&gt;

    &lt;recent_actions&gt;
        
    &lt;/recent_actions&gt;

    &lt;current_plan&gt;
        
    &lt;/current_plan&gt;

&lt;/state_snapshot&gt;
</code></pre>
<p>将历史记录输入到这个提示词的 LLM 中，会有压缩后的 state_snapshot 中的关键信息输出，我们一起来看看这 5 点关键信息的含义</p>
<ol>
<li><strong>Overall Goal（总体目标）</strong>：描述用户的高层目标，让下一轮的 AI 快速理解用户想达成什么</li>
<li><strong>Key Knowledge（关键知识）</strong>：关键的信息和约束条件，例如：项目中的测试命令是 <code>npm test</code>，就不会使用 <code>jest</code> 命令</li>
<li><strong>File System State（文件系统状态）</strong>：记录哪些文件被创建、修改和删除</li>
<li><strong>Recent Actions（最近操作）</strong>：保留最近的操作及其结果</li>
<li><strong>Current Plan（当前计划）</strong>：整理当前计划的状态，总共有哪些任务，哪些任务是完成的，哪些任务是没有完成的</li>
</ol>
<h2>三、上下文压缩 - 工具消息裁剪</h2>
<p>上面说的两种都是直接使用大模型来进行压缩的，主要区别只是提示词和细微的流程不同，但压缩的策略都是由模型来自主根据提示词判断</p>
<p>这一节我们使用的是<strong>上下文压缩策略 - 清理工具的输入和输出</strong>以达到上下文压缩的目的，这个设计理念在 Claude 团队中得到验证</p>
<blockquote>
<p><strong>Context editing</strong> automatically clears stale tool calls and results from within the context window when approaching token limits. As your agent executes tasks and accumulates tool results, context editing removes stale content while preserving the conversation flow, effectively extending how long agents can run without manual intervention. This also increases the effective model performance as Claude focuses only on relevant context.<br />
上下文编辑在接近 token 限制时，会自动清除上下文窗口中的过时工具调用和结果。当你的代理执行任务并积累工具结果时，上下文编辑会移除过时内容，同时保留对话流程，有效延长代理无需人工干预即可运行的时间。这也有助于提升有效模型性能，因为 Claude 只会关注相关上下文</p>
</blockquote>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-49.BD1QfVdM_ke5z.webp" alt="上下文中工具调用的 Token 占比" /></p>
<p>我们仔细回顾一下上下文管理和这些大模型应用，最消耗上下文的工具是读取工具，会大量读取文件和内容，在使用 ClaudeCode 这类工具的时候，在任务完成之前会先读取相关的内容，其实用户本身的输入和模型本身的输出并不多，大部分都是工具的调用，尤其是工具的输出</p>
<p>在所有的上下文组成中，要按照分类来裁剪的话，首先移除工具的相关上下文是合理的</p>
<p>具体的代码实现思路：</p>
<ul>
<li>从历史记录中将工具的输入和输出筛选出来</li>
<li>判断是否全部删除还是保留最近的 N 次工具调用</li>
<li>得到合适的上下文</li>
</ul>
<pre><code>//LLM 的消息格式
export interface Message {
  role: 'system' | 'user' | 'assistant' | 'tool';
  content: string;
  tool_calls?: ToolCall[];
  tool_call_id?: string;
}

// 工具调用结构
export interface ToolCall {
  id: string;
  type: 'function';
  function: {
    name: string;
    arguments: string;
  };
}

// 上下文压缩配置
export interface CompressionOptions {
  // 是否启用压缩
  enabled: boolean;
  // 保留最近 N 轮工具调用（0 表示全部移除）
  keepLastToolRounds?: number;
}

/**
 * 压缩上下文，清理过时的工具调用和结果
 * @param messages 待压缩的消息数组
 * @param options 压缩配置
 * @returns 压缩后的消息数组
 */
export function compressContext(
  messages: Message[],
  options: CompressionOptions
): Message[] {
  if (!options.enabled) {
    return messages;
  }

  const { keepLastToolRounds = 1 } = options;

  // 识别所有工具调用轮次
  const toolRounds = identifyToolRounds(messages);

  // 确定要保留的轮次
  const toolRoundsToKeep =
    keepLastToolRounds &gt; 0 ? toolRounds.slice(-keepLastToolRounds) : [];
  const indicesToKeep = new Set&lt;number&gt;();

  // 标记要保留的消息索引
  for (const round of toolRoundsToKeep) {
    round.indices.forEach(idx =&gt; indicesToKeep.add(idx));
  }

  // 过滤消息 - 保留下来的消息数组（大部分是去除工具的调用和输出）
  const compressedMessages: Message[] = [];

  for (let i = 0; i &lt; messages.length; i++) {
    const message = messages[i];

    // 保留系统消息
    if (message.role === 'system') {
      compressedMessages.push(message);
      continue;
    }

    // 保留标记的工具轮次消息
    if (indicesToKeep.has(i)) {
      compressedMessages.push(message);
      continue;
    }

    // 保留非工具相关的消息
    const isToolRelated =
      (message.role === 'assistant' &amp;&amp; message.tool_calls) ||
      message.role === 'tool';

    if (!isToolRelated) {
      compressedMessages.push(message);
      continue;
    }
  }

  return compressedMessages;
}

/** 工具调用轮次结构 */
interface ToolRound {
  /** 包含工具调用的 assistant 消息索引 */
  assistantIndex: number;
  /** 工具结果消息索引数组 */
  toolIndices: number[];
  /** 该轮次的所有消息索引 */
  indices: number[];
}

/**
 * 识别消息历史中的工具调用轮次
 * @param messages 消息数组
 * @returns 工具调用轮次数组
 */
function identifyToolRounds(messages: Message[]): ToolRound[] {
  const rounds: ToolRound[] = [];
  let currentRound: ToolRound | null = null;

  for (let i = 0; i &lt; messages.length; i++) {
    const message = messages[i];

    // 开始新的工具调用轮次
    if (message.role === 'assistant' &amp;&amp; message.tool_calls) {
      // 保存上一轮次
      if (currentRound) {
        currentRound.indices = [
          currentRound.assistantIndex,
          ...currentRound.toolIndices,
        ];
        rounds.push(currentRound);
      }

      // 创建新轮次
      currentRound = {
        assistantIndex: i,
        toolIndices: [],
        indices: [],
      };
      continue;
    }

    // 收集工具结果
    if (message.role === 'tool' &amp;&amp; currentRound) {
      currentRound.toolIndices.push(i);
      continue;
    }

    // 遇到非工具消息时结束当前轮次
    if (currentRound &amp;&amp; message.role !== 'tool') {
      currentRound.indices = [
        currentRound.assistantIndex,
        ...currentRound.toolIndices,
      ];
      rounds.push(currentRound);
      currentRound = null;
    }
  }

  // 保存最后一个轮次
  if (currentRound) {
    currentRound.indices = [
      currentRound.assistantIndex,
      ...currentRound.toolIndices,
    ];
    rounds.push(currentRound);
  }

  return rounds;
}

/**
 * 获取压缩统计信息
 * @param original 原始消息数组
 * @param compressed 压缩后的消息数组
 * @returns 统计信息对象
 */
export function getCompressionStats(
  original: Message[],
  compressed: Message[]
): {
  originalCount: number;
  compressedCount: number;
  removedCount: number;
  compressionRatio: number;
} {
  const originalCount = original.length;
  const compressedCount = compressed.length;
  const removedCount = originalCount - compressedCount;
  const compressionRatio =
    originalCount &gt; 0 ? compressedCount / originalCount : 1;

  return {
    originalCount,
    compressedCount,
    removedCount,
    compressionRatio,
  };
}
</code></pre>
<h2>四、上下文压缩 - 中间和最旧策略的选择</h2>
<p>我在一个不错项目中看到它的压缩策略非常优雅，没有借助大模型进行压缩，是依靠判断算法来压缩上下文，这种方式在我看来是非常可控的，但是开发难度会比较麻烦</p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-50.CjgfkBoD_1V3wTl.webp" alt="中间与最旧移除策略" /></p>
<p>这种上下文压缩的核心：根据当前消息的一些属性和状态来判断使用哪种压缩策略效果是最好的</p>
<p>其提供了三种移除压缩策略的选择：</p>
<ol>
<li><strong>中间移除策略</strong>：保留对话的开始和结束的部分，移除中间的消息</li>
<li><strong>最旧移除策略</strong>：优先移除最早的消息、保留较新的消息</li>
<li><strong>混合移除策略</strong>：智能结合中间移除策略和最旧移除策略</li>
</ol>
<h3>4.1、压缩策略的选择方法</h3>
<p>总共有三种方法来判断最终使用那种压缩策略，选择的模式是一层一层往下的</p>
<ol>
<li>第一层：基于供应商｜模型的选择</li>
<li>第二层：基于对话特征的选择</li>
<li>第三层：置信度的判断</li>
</ol>
<h3>4.2、根据供应商和模型进行选择</h3>
<table>
<thead>
<tr>
<th>提供商</th>
<th>模型</th>
<th>推荐策略</th>
<th>原因</th>
</tr>
</thead>
<tbody>
<tr>
<td>OpenAI</td>
<td>GPT-4</td>
<td>hybrid - 混合策略</td>
<td>平衡的开始和结束保留，适合通用对话</td>
</tr>
<tr>
<td>OpenAI</td>
<td>O1</td>
<td>middle-removal - 中间移除策略</td>
<td>更高的保留数量，适合需要更多上下文的模型</td>
</tr>
<tr>
<td>Anthropic</td>
<td>所有</td>
<td>oldest-removal - 最旧移除策略</td>
<td>保留更多结束消息，适合 Anthropic 的对话风格</td>
</tr>
<tr>
<td>Google</td>
<td>1.5</td>
<td>middle-removal - 中间移除策略</td>
<td>大上下文模型，需要更保守的压缩</td>
</tr>
<tr>
<td>LMStudio/Ollama</td>
<td>所有</td>
<td>hybrid  - 混合策略</td>
<td>本地模型通常有较小上下文，需要更激进的压缩</td>
</tr>
</tbody>
</table>
<h3>4.3、根据对话特征选择</h3>
<p><strong>🌟当第一步输出的压缩策略是混合策略的时候，才会进行这一步，这一步是为了根据对话特征判断选择中间移除策略还是最旧移除策略</strong></p>
<p>该方法首先要根据历史记录获取出来对话特征这些数据</p>
<pre><code>async analyzeConversation(
    messages: Message[],
    currentTokenCount: number, //当前消息的 Token
    targetTokenCount: number //压缩后的消息 Token
):  {
    const totalMessages = messages.length;  // 消息总数
    const avgMessageLength = currentTokenCount / totalMessages;  // 平均消息长度
    const compressionRatio = targetTokenCount / currentTokenCount;  // 压缩比例

    // 分析消息分布
    const recentMessages = messages.slice(-5);  // 获取最近 5 条消息
    const recentTokens = calculateTotalTokens(recentMessages);  // 计算最近消息的令牌数
    const recentRatio = recentTokens / currentTokenCount;  // 最近消息令牌占比

    // 分析对话模式
    const hasLongMessages = messages.some(m =&gt; (m.tokenCount || 0) &gt; 300);  // 是否有长消息
    const hasSystemMessages = messages.some(m =&gt; m.role === 'system');  // 是否有系统消息
    const hasToolMessages = messages.some(m =&gt; m.role === 'tool');  // 是否有工具消息

    // 确定压缩严重程度
    const compressionSeverity = this.getCompressionSeverity(compressionRatio);  // 压缩严重程度

    // ... 决策逻辑 ...
}

async getCompressionSeverity(compressionRatio: number): 'light' | 'moderate' | 'heavy' {
    if (compressionRatio &gt; 0.8) return 'light';      // 轻度压缩（目标&gt;80%）
    if (compressionRatio &gt; 0.6) return 'moderate';  // 中度压缩（目标&gt;60%）
    return 'heavy';                                 // 重度压缩（目标≤60%）
}
</code></pre>
<p>关于 targetTokenCount 这个变量，在执行真正的压缩之前，会进行对话记录的判断，看看是否有压缩的必要，举一个例子：</p>
<ul>
<li>当前的消息记录 Token 为：100K</li>
<li>使用 GPT-4o 模型的最佳 Token 数是：80K（有可能 GPT-4o 的最大 Token 是 128K）</li>
</ul>
<p>所以我们可以知道要移除大概 20K 的 Token 才可以符合要求</p>
<p>当我们得到了这些对话特征之后，我们可以根据这些变量进行判断，以此来确定使用哪一种方安，判断的依旧如下：</p>
<p><strong>规则一：轻度压缩且对话较短 - 中间移除策略</strong></p>
<pre><code>if (compressionSeverity === 'light' &amp;&amp; totalMessages &lt; 20) {
    recommendedStrategy = 'middle-removal';
    confidence = 0.8;
}
</code></pre>
<p>选择中间压缩策略的原因是：</p>
<ul>
<li>在短对话中，对话的开始和结束通常包含最重要的上下文</li>
<li>轻度压缩意味着只需要移除少量消息</li>
<li>移除中间部分可以最大程度地保留对话的完整性，因为短对话的中间部分通常包含较少的关键信息</li>
</ul>
<p><strong>规则二：重度压缩且对话较长 - 最旧移除策略</strong></p>
<pre><code>else if (compressionSeverity === 'heavy' &amp;&amp; totalMessages &gt; 30) {
    recommendedStrategy = 'oldest-removal';
    confidence = 0.9;
}
</code></pre>
<p>选择最旧移除策略的原因：</p>
<ul>
<li>在长对话中，较新的消息通常更相关和重要</li>
<li>重度压缩需要移除大量消息，保留最新消息可以确保对话的连续性</li>
</ul>
<p><strong>规则三：最近消息 Token 占比高 - 中间移除策略</strong></p>
<pre><code>else if (recentRatio &gt; 0.4) {
    recommendedStrategy = 'middle-removal';
    confidence = 0.7;
}
</code></pre>
<p>选择中间移除策略的原因：</p>
<ul>
<li>当前消息已经占用大量的 Token，这些消息很可能包含重要的信息</li>
<li>使用中间移除策略，可以在保留最近重要消息的同时达到压缩的目标</li>
</ul>
<blockquote>
<p>在这个规则场景下，最旧移除策略也是可以的，所以这里可以细分一下</p>
<ul>
<li>如果是有系统提示等关键的开头信息，使用中间移除策略保存开头和结尾</li>
<li>如果是纯对话的场景，使用最旧移除保存最近消息</li>
</ul>
</blockquote>
<p><strong>规则四：包含长消息且需要显著压缩 - 最旧移除策略</strong></p>
<pre><code>else if (hasLongMessages &amp;&amp; compressionSeverity !== 'light') {
    recommendedStrategy = 'oldest-removal';
    confidence = 0.6;
}
</code></pre>
<p>选择最旧移除策略的原因：</p>
<ul>
<li>长消息通常包含重要的信息，保留这些长消息很有效</li>
</ul>
<p><strong>规则五：包含工具或系统消息 - 中间移除策略</strong></p>
<pre><code>else if (hasToolMessages || hasSystemMessages) {
    recommendedStrategy = 'middle-removal';
    confidence = 0.7;
}
</code></pre>
<p>选择中间移除策略的原因：</p>
<ul>
<li>大部分的工具执行都会在中间，例如这样的链式：输入=&gt; 任务分配 =&gt; 执行=&gt; 结果，所以移除中间的话，相对完整的保留上下文的框架</li>
</ul>
<h3>4.4、使用自适应方法选择策略</h3>
<p>在第二步中的每一个规则都会输出一个置信度、这个置信度是用来作为策略的可信度的</p>
<ul>
<li><code>confidence = 0.8</code>：高度可信，规则条件明确，策略选择合理</li>
<li><code>confidence = 0.9</code>：非常高度可信，规则条件非常明确，策略选择非常合理</li>
<li><code>confidence = 0.7</code>：中等可信，规则条件相对明确，但可能有例外情况</li>
<li><code>confidence = 0.6</code>：低度可信，规则条件不够明确，策略选择可能有争议</li>
</ul>
<p>🌟🌟 当上面在进行根据对话特征选择的时候，<strong>输出置信度低于 0.6 的时候</strong>，就会启动系统的自适应的方法</p>
<p>具体的流程是：</p>
<ol>
<li>系统会分别执行两种压缩策略，也就是最旧移除策略和中间移除策略都执行一遍</li>
<li>计算每种策略结果的效率分数（综合考虑令牌减少和消息保留）</li>
<li>选择效率分数更高的策略作为最终结果</li>
</ol>
<p><strong>效率计算方法</strong>：</p>
<p><strong>效率 = 令牌减少 (60% 权重) + 消息保留 (40% 权重)</strong></p>
<ul>
<li>令牌减少占 60% 的权重（更重要的目标）</li>
<li>消息保留占 40% 的权重（次要但重要的目标）</li>
</ul>
<h4>实际场景：</h4>
<p>假设有一个包含 15 条消息的对话，总令牌数为 9000，需要压缩到 6000 个令牌。</p>
<ol>
<li>中间移除策略的结果：</li>
</ol>
<ul>
<li>压缩后令牌数：6200</li>
<li>保留消息数：12</li>
<li>移除消息数：3</li>
</ul>
<ol>
<li>最旧移除策略的结果：</li>
</ol>
<ul>
<li>压缩后令牌数：5800</li>
<li>保留消息数：10</li>
<li>移除消息数：5</li>
</ul>
<p><strong>效率计算过程</strong></p>
<ol>
<li>中间移除策略的效率计算</li>
</ol>
<pre><code>// 计算压缩比
const compressionRatio = 6200 / 9000 = 0.689;

// 计算令牌减少率
const tokenReduction = 1 - 0.689 = 0.311;

// 计算消息保留率
const messagePreservation = 12 / 15 = 0.8;

// 计算效率分数
const efficiency = 0.311 * 0.6 + 0.8 * 0.4 = 0.1866 + 0.32 = 0.5066;
</code></pre>
<ol>
<li>最旧移除策略的效率计算</li>
</ol>
<pre><code>// 计算压缩比
const compressionRatio = 5800 / 9000 = 0.644;

// 计算令牌减少率
const tokenReduction = 1 - 0.644 = 0.356;

// 计算消息保留率
const messagePreservation = 10 / 15 = 0.667;

// 计算效率分数
const efficiency = 0.356 * 0.6 + 0.667 * 0.4 = 0.2136 + 0.2668 = 0.4804;
</code></pre>
<p><strong>结果选择</strong></p>
<pre><code>// 比较效率分数
if (middleEfficiency &gt;= oldestEfficiency) {  // 0.5066 &gt;= 0.4804
    return middleResult;  // 选择中间移除策略的结果
} else {
    return oldestResult;  // 选择最旧移除策略的结果
}
</code></pre>
<p>在这个例子中，虽然最旧移除策略减少了更多的令牌（35.6% vs 31.1%），但中间移除策略保留了更多的消息（80% vs 66.7%）。由于系统综合考虑了令牌减少和消息保留，并且消息保留的权重较高，因此最终选择了中间移除策略的结果。</p>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[Agent 的评估：评估方案和方法]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/agent-eval-overview/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/agent-eval-overview/</guid>
            <pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[为什么 Agent 评估这么重要？评估的四大组成、完整流程和三种评分方法（代码评分、人工评分、模型评分）的实战详解。]]></description>
            <content:encoded><![CDATA[<h2>前言</h2>
<ul>
<li>《Claude-Cookbooks》：https://github.com/anthropics/claude-cookbooks</li>
<li>LangFuse 的文档：https://langfuse.com/docs/evaluation/overview</li>
<li>promptfoo 框架：https://github.com/promptfoo/promptfoo</li>
</ul>
<h2>一、Agent 评估为什么这么重要</h2>
<p>LLM 的输出是存在不可控因素的，而对于一个线上生产级别的大模型应用来说，稳定性是最重要的，成熟的评估方案不仅可以让大模型应用更加稳定，同时也可以发现模型的潜力和边界，以此更好的迭代大模型应用</p>
<p>有几句真实引述可以参考。以此来解释为什么评估很重要</p>
<blockquote>
<ol>
<li>团队难以有效的评估模型性能是 LLMs 生产应用案例的最大障碍，同时也使提示词的设计变成艺术而非科学</li>
<li>尽管评估需要大量时间，但提前进行评估将长期节省开发人员的时间，并使更好的产品能更快地推出</li>
</ol>
</blockquote>
<p>评估对于大模型应用开发是有好处的，可以量化一些模型的边界能力：</p>
<ol>
<li>✏️ <strong>迭代式提示词优化</strong>：我们的 V2 版本的提示词是否比 V1 版本更好呢？</li>
<li>✏️ <strong>部署上线前后提示词变更质量的保证</strong>：我们的最新提示词更新是否导致了性能下降？</li>
<li>✏️ <strong>客观模型的对比</strong>：我们更换为更高级的模型时，是否可以维持或提升当前的评估性能？</li>
<li>✏️ <strong>潜在的成本节约</strong>：当我们更换为更快、成本更低的模型时是否可以维持当前的评估性能？</li>
</ol>
<p>设计完整的评估方案并且准确的执行，不仅有益于大模型应用开发的效率，同时也可以帮助团队探索出模型的边界能力，为极大释放模型的潜力提供方向</p>
<p>接下来就一起来看看评估方案的主要元素有哪些，同时评估的流程是什么，还有评估的方法</p>
<h2>二、评估的组成</h2>
<p>一个设计良好的评估方案由四个主要组成部分构成：</p>
<ol>
<li><strong>示例输入</strong>：这里是给模型的指令或问题，<strong>关键是要设计出能够准确代表你的应用在实际使用中会遇到的各类场景的输入</strong></li>
<li><strong>标准答案</strong>：正确的或理想的回答，作为模型输出的基准，创建高质量的标准答案通常需要领域专家的参与，以确保准确性和相关性</li>
<li><strong>模型输出</strong>：这个是 LLM 基于示例输入实际生成的回答，这个就是你要拿来与黄金答案对比的评估内容</li>
<li><strong>分数</strong>：一个定量或定性的值，代表模型在该特定输入上的表现，评分方法可以根据你的任务性质和选择的评分方法而有所不同</li>
</ol>
<p>关于准备的<strong>示例输入和标准答案至少需要超过 100 组</strong>才会有评估的意义和参考价值</p>
<h2>三、评估的流程</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-46.C_qF9L34_Z17JCOO.webp" alt="评估流程" /></p>
<p>上图就是评估的完整流程的示意图，下面我详细解释一下各步骤中的关键</p>
<ol>
<li>首先我们要准备好测试用例，这个是由示例输入和标准答案组成</li>
<li>然后将测试用例分为两批，80% 作为开发集测试用例，20% 作为留存集测试用例</li>
<li>根据你自己的感觉设计第一版本的提示词</li>
<li>在开发集上面进行测试</li>
<li>当效果很差的时候，就根据测试结果优化提示词，循环往复，直到提示词合格</li>
<li>这个时候你再使用之前准备好的留存集测试，这一步是验证提示词的泛化</li>
<li>当<strong>留存集的测试结果和开发集的测试结果差距</strong>小于 10%（具体的你可以自己定，我这里只是举一个例子）左右，就可以通过</li>
<li>如果差距大于 10%，就说明提示词和开发集的测试用例严重过拟合，重新再回到优化提示词的那一步，循环往复，直到留存集测试结果合格</li>
</ol>
<p>在评估流程中，两步测试用例的结果是主要的，<strong>开发集准确率和留存集的准确率</strong>，但是还有两个评估因素可以考虑使用</p>
<ul>
<li><strong>边缘案例覆盖</strong>：评估在极端输入中，模型的表现如何</li>
<li><strong>性能测试</strong>：评估模型的响应时间</li>
</ul>
<p>当然在整个评估流程的准备和执行中，最重要的两点：</p>
<ol>
<li>编写评估问题和标准答案：也就是标准答案的准备，如果让人工来编写问题和标准答案会非常耗时，成本也非常高，但这个成本是一次性的，编写好的问题和标准答案可以重复利用</li>
<li>评分运行产生的持续成本：我们会持续高频繁的运行评估，如果采用大模型来评分，那么这一步的模型成本是存在的，所以我们要尽量构建快速且经济地评估体系核心</li>
</ol>
<h2>四、评估的方法</h2>
<p>常见的三种评估方法是：</p>
<ul>
<li>基于代码评分：使用标准代码来匹配和判断模型的输出</li>
<li>人工评分：人工手动的查看模型生成的答案进行打分</li>
<li>基于模型的评分：由一个更高级的模型来对于输出进行评价</li>
</ul>
<p>三种评分方法中可以首先考虑使用模型评分和代码评分，最后才考虑人工评分，因为人工评分相比前面两种方式其成本大，周期长</p>
<h3>4.1、基于代码评分</h3>
<ol>
<li>📝 <strong>特点</strong>：基于代码的评分使用程序化方法来评估模型的输出。这种方法适用于具有明确、客观标准的任务。</li>
<li><strong>优势</strong>：基于代码的评分的主要优势在于其速度和可扩展性。一旦设置完成，它能够快速且一致地处理数千次评估。然而，它在处理细微差别或主观性回答方面的能力有限。<strong>常见的基于代码的评分技术包括精确字符串匹配、关键词存在性检查以及使用正则表达式进行模式匹配</strong></li>
<li><strong>形式</strong>：
<ol>
<li>精确字符串匹配评分：模型的输出必须与标准答案完全一致</li>
<li>关键词存在性检查：这种方法用于判断模型的输出是否包含某些关键单词和短语</li>
<li>正则表达式：我们可以定义正则表达式来检查复杂的文本模式</li>
</ol>
</li>
</ol>
<h4>4.1.1、代码评分的例子</h4>
<p>因为案例比较简单，为了方便展示整个评估流程，选择了模型能力相对弱的 <code>Qwen2-7B-Instruct</code></p>
<p>第一步：我们先准备好一个评估用例数据集**（示例输入和标准输出）**，这里我们取测试主干，暂时不进行留存集的测试步骤</p>
<pre><code>let testCases = [
  { id: 1, text: '太棒了！非常满意，五星好评！', expected: '积极', reason: '明确的正面词汇' },
  { id: 2, text: '物流很快，质量超出预期，强烈推荐', expected: '积极', reason: '多个正面描述' },
  { id: 3, text: '垃圾产品，完全不能用，退款了', expected: '消极', reason: '明确的负面词汇' },
  { id: 4, text: '质量太差，客服态度恶劣，不推荐', expected: '消极', reason: '多个负面描述' },
  { id: 5, text: '今天收到货了，包装还可以', expected: '中性', reason: '陈述事实，无明显情感倾向' },
  { id: 6, text: '产品是蓝色的，重量 500 克左右', expected: '中性', reason: '纯客观描述' },
  { id: 7, text: '呵呵，真是"完美"的体验呢', expected: '消极', reason: '引号表示讽刺' },
  { id: 8, text: '好得不得了，好到我想扔掉', expected: '消极', reason: '前半句是假好评，后半句才是真实情感' },
  { id: 9, text: '虽然价格有点贵，但是质量真的很好，值得购买', expected: '积极', reason: '转折结构，整体积极' },
  { id: 10, text: '外观设计不错，但是用了一天就坏了', expected: '消极', reason: '先肯定后否定，整体消极' },
  { id: 11, text: '买了三次了，每次都回购', expected: '积极', reason: '没有褒义词但多次回购说明满意' },
  { id: 12, text: '用了两天就不想再用了', expected: '消极', reason: '没有贬义词但表达了放弃使用' },
];
</code></pre>
<p>第二步：我们再准备一个初版的提示词</p>
<pre><code>let promptV1 = (text: string) =&gt; `
判断以下文本的情感倾向，回答"积极"、"消极"或"中性"。

文本：${text}

只回答一个词：积极、消极或中性。
`;
</code></pre>
<p>第三步：运行初版提示词得出评估分数</p>
<pre><code>提示词 V1 (简单版) 评估结果汇总:
总测试用例数：12
成功用例数：10
失败用例数：2
准确率：83.33%

失败用例详情:
  用例 5: 期望中性，实际积极
    原因：陈述事实，无明显情感倾向
  用例 7: 期望消极，实际积极
    原因：引号表示讽刺，简单提示词看到"完美"会判断为积极
</code></pre>
<p>第四步：根据评估结果优化提示词</p>
<pre><code>let promptV2 = (text: string) =&gt; `
你是一个情感分析专家，判断以下文本的情感倾向。

分类标准：
- 积极：表达满意、赞赏、推荐等正面情感
- 消极：表达不满、失望、批评等负面情感
- 中性：客观陈述事实，无明显情感倾向

注意事项：
- 注意"虽然...但是..."这类转折句，以后半句为准
- 注意带引号的词可能是反讽

文本：${text}

只回答一个词：积极、消极或中性。
`;
</code></pre>
<p>第五步：执行优化后的提示词，准确率提升</p>
<pre><code>提示词 V2 (规则版) 评估结果汇总:
总测试用例数：12
成功用例数：12
失败用例数：0
准确率：100.00%

对比结果:
V1 准确率：83.33%
V2 准确率：100.00%
</code></pre>
<p>第六步：当准确率得到提升合格之后，就可以进入到留存率的测试集中或者评估通过</p>
<h3>4.2、基于人工评分</h3>
<ol>
<li>📝<strong>特点</strong>：对于需要细致理解或主观判断的任务，基于人类的评分仍然是标准答案。</li>
<li><strong>优势</strong>：人工评分在评估诸如语 tone、创造力、复杂推理或事实性等方面的能力上表现优异，尤其是在处理开放式任务时。<strong>其缺点是耗时且可能成本高昂，尤其是在大规模评估中。此外，它还可能受到不同评分者之间不一致性的影响。</strong></li>
<li><strong>形式</strong>：
<ol>
<li>专家评审：领域内的专家评估回答的准确性和深度</li>
<li>用户体验小组：一个小组评估输出内容的清晰度、帮助性、参与度以及其他基于人类判断的方面</li>
</ol>
</li>
</ol>
<h3>4.3、基于模型评分</h3>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/image-47.D3AfmE8r_Z1agN29.webp" alt="基于模型评分" /></p>
<ol>
<li>📝<strong>特点</strong>：基于 LLM 的评分方法介于基于代码和基于人工的方法之间。这种方法使用另一个 LLM（有时是同一个）来评估输出。</li>
<li><strong>优势</strong>：与基于代码的评分相比，这种方法可以处理更复杂和主观的评估，同时比人工评分更快、更具可扩展性。然而，它需要高超的提示工程技巧来确保可靠的结果，并且评分 LLM 引入自身偏见的风险始终存在。</li>
<li><strong>形式</strong>：
<ol>
<li>摘要质量：这个摘要有多简洁和准确</li>
<li>语气评估：该回复是否符合我们品牌指南或语气</li>
</ol>
</li>
</ol>
<h4>4.3.1、评估模型的提示词书写</h4>
<p>在基于模型评分的方法下，最核心的是<strong>评估模型，其对应的也是需要为评估模型编写一份提示词，关于这个提示词的书写思路可以参考：</strong></p>
<ul>
<li>原始提示或问题</li>
<li>我们想要评估的模型输出</li>
<li>一套用于评估的标准或指南</li>
<li>关于如何评估和给响应结果打分的说明</li>
</ul>
<p>常见的模型评估的标准或指南：</p>
<ol>
<li>这个回应带有多少歉意？</li>
<li>根据所提供的上下文，该回应在事实上是否准确？</li>
<li>这个回复是否过度提及自身上下文的信息</li>
<li>这个回复是否真正恰当回答了问题？</li>
<li>这个输出与我们的语气｜品牌｜风格指南的契合度如何？</li>
</ol>
<h4>4.3.2、评估模型的定位</h4>
<p>一个合格的评估模型想要保持客观中立的态度，目前的很多模型默认情况下都是友好的，有偏向道歉的倾向</p>
<p>所以在书写评估模型的提示词中，需要加一段观点和角度的限制</p>
<p><strong>"不道歉和不使用道歉的语言，要客观中立"</strong></p>
<h4>4.3.3、评估案例</h4>
<p>步骤 1：用户提问</p>
<blockquote>
<p>输入："为儿童玩具店写一句宣传语"</p>
</blockquote>
<p>步骤 2：被测试模型输出</p>
<blockquote>
<p>"本店提供高品质玩具，欢迎选购。"</p>
</blockquote>
<p>步骤 3：评估 LLM 打分</p>
<blockquote>
<p>评估提示：</p>
<ul>
<li>评估是否符合儿童友好、活泼的语气。</li>
<li>总分 10 分</li>
</ul>
</blockquote>
<p>步骤 4：评估结果</p>
<blockquote>
<ul>
<li>得分：4/10</li>
<li>理由：语气过于正式严肃，不符合儿童玩具店的活泼定位</li>
</ul>
</blockquote>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
        <item>
            <title><![CDATA[为你的 Agent 集成 Skill 系统]]></title>
            <link>https://wakeup-jin-blog.netlify.app/posts/agent-skill-integration/</link>
            <guid isPermaLink="false">https://wakeup-jin-blog.netlify.app/posts/agent-skill-integration/</guid>
            <pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[为自己开发的 Agent 添加 Skill 支持时的核心步骤：发现、解析、使用、管理，以及渐进式披露策略的实践指南。]]></description>
            <content:encoded><![CDATA[<p>为自己开发的 Agent 添加 Skill 支持时，开发的核心步骤：<strong>发现、解析、使用、管理</strong></p>
<ol>
<li>发现：你的 Skill 存储在哪里，本地还是云端，项目级和用户级的优先级是什么，如何确定该文件夹是一个 Skill</li>
<li>解析：将 SKILL.md 的元信息解析出来</li>
<li>使用：Agent 如何使用解析出来的元信息，是系统提示词还是工具描述，后续的渐进式披露策略的执行，是使用读取工具还是使用内部的激活工具</li>
<li>管理：如何维持加载进入上下文的 Skill 的有效性，上下文压缩的时候 Skill 的内容是否需要保护</li>
</ol>
<p>🌟 <strong>开发的核心原则也是 Skill 的核心特点：渐进式披露</strong></p>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/BH9kbQf7GoZGUXx5RHDcrzuDnfh.C8QTNaTo_2uvR7x.webp" alt="Skill 的渐进式加载" /></p>
<ul>
<li>第一层披露：在会话启动的时候，将**元信息（名称 + 描述）**加载到上下文中</li>
<li>第二层披露：在 Skill 激活的时候，也就是 Agent 根据用户输入和元信息匹配到相应的 Skill，将<strong>完整的 SKILL.md 内容</strong>加载到上下文中</li>
<li>第三层披露：当 SKILL.md 的正文内容加载到上下文之后，Agent 根据任务的复杂情况来选择加载更详细的指导说明，也就是<strong>脚本、参考资料、静态资源</strong>这三种可按需加载的资源</li>
</ul>
<h2>一、发现</h2>
<p>Agent 是需要从相应的文件目录中发现运行环境有哪些 Skill，大部分 Agent 是运行在本地环境的，所以我们重点说一下本地环境的 Skill 发现</p>
<p>对于 Skill 的文件目录的范围是分为两种的：<strong>用户全局范围和项目局部范围</strong></p>
<ul>
<li>项目局部范围：只对当前项目生效的 Skill，例如：前端设计 Skill、React 最佳实践 Skill 等</li>
<li>用户全局范围：对用户所有的项目生效的 Skill，例如：find-skills（发现 Skill）、pptx（生成 PPT 的 Skill）</li>
</ul>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/XF5ybPkrbolYg6xGvAfciXZEnvd.C3ajlNAh_25AgOB.webp" alt="Skill 的发现" /></p>
<p>具体的 Skill 的目录模块是这样的：</p>
<ol>
<li><code>&lt;project&gt;/.&lt;agent-client&gt;/skills/</code>：项目范围的可用 Skill</li>
<li><code>&lt;project&gt;/.agents/skills/</code></li>
<li><code>~/.&lt;agent-client&gt;/skills/</code>：全局范围的可用 Skill</li>
<li><code>~/.agents/skills/</code></li>
</ol>
<p>从上面的路径中可以发现存在 <code>/.agents/</code> 的文件路径，这是因为该路径已经成为跨各种不同的客户端技能共享的广泛采用的约定，例如：</p>
<blockquote>
<p>别人可能开发了 a-agent-client，它的 Skill 的目录是 <code>~/.a-agent-client/skills/</code>，你这个时候开发的 b-agent-client 想要使用 a 的 Skill 目录，你就要做兼容，读取目录的时候特定读取<code>~/.a-agent-client</code>，一个当然不要紧，那假如有很多个呢？并且路径都不一样，那岂不是每次都要更新这个东西，
所以大家就采用一种约定俗成的规范，无论什么客户端，都可以给安装的 Skill 提供 <code>~/.agents/skills/</code> 这个目录，那么后续开发的时候就都可以默认读取一下这个目录，<strong>达到兼容各种客户端统一规范的目的</strong></p>
</blockquote>
<p><strong>🌴 所以读取/.agents/的文件路径意味着其他符合规范的客户端安装的 Skill 会自动对你的客户端可见，相反别人也可以自动可见你的</strong></p>
<p><strong>有时候会在用户全局范围和项目局部范围出现相同的 Skill</strong>，这个时候优先级判断是：</p>
<ul>
<li>项目级优先级大于用户级</li>
<li>相同范围内，按照发现顺序排优先级</li>
</ul>
<p>设计信任检查的考虑是因为，有一些项目拉取下来会存在一些 skill，可能这些 skill 是恶意不安全的，可能会泄漏你的密钥等隐私性的东西。</p>
<p>所以在 Agent 的配置文件中可以考虑设计一层 skill 信任检查，在项目范围内，只有受信任的 Skill 才可以加载使用，这个是在业务逻辑层面进行判断的</p>
<p>🎃 一点开发技巧的小补充：</p>
<p>在读取 skills 文件夹的时候，要**查找包含名为 SKILL.md 的子目录，**这个才是有效的 Skill，是符合 Skill 规范的，一些开发读取 Skill 路径技巧：</p>
<ul>
<li>要跳过不包含 skills 的目录，例如：node_modules/这种依赖项文件</li>
<li>可以考虑遵守项目的.gitgnore 文件，避免扫描一些构建产物，类似于 dist/文件夹</li>
<li>不要一直嵌套循环的深度搜索，要设置最大搜索层数 (5-6 层)，同时设置最大文件数量</li>
</ul>
<h2>二、解析</h2>
<p>在 Agent 会话开始的阶段，是需要将 Skill 的元信息加载到上下文中的，所以我们在获取到 Skill 的文件路径之后，下一步就是需要解析 SKILL.md 的元信息啦</p>
<p><strong>SKILL.md 的文件包含两部分：</strong></p>
<ol>
<li>以 <code>---</code> 分隔符分开的 YAML 前置元数据</li>
<li>以结束分隔符分开的 MD 格式的内容</li>
</ol>
<p>这些元信息的字段和约束如下：</p>
<ul>
<li>name（必需）：表示技能的名称，最多 64 字符，仅允许小写字母、数字、连字符。不得以连字符开头或结尾</li>
<li>description（必需）：对于该技能的描述，最多 1024 字符，不能为空</li>
<li>license（可选）：对于许可证名称或者许可证文件的引用</li>
<li>compatibility（可选）：表示环境的要求（目标产品、系统包、网络访问）</li>
<li>metadata（可选）：附加元数据</li>
<li>allowed-tools（可选）：技能允许执行的工具列表</li>
</ul>
<p>那么详细的开发步骤为：</p>
<ol>
<li>找到 SKILL.md 文件分隔符的开始和结尾部分</li>
<li>解析中间的 YAML 代码块，提取出来 name 和 description 字段，还有其他的可选字段</li>
<li>而结尾分隔符后面的 MD 语法格式的内容就是 SKILL.md 的正文内容</li>
</ol>
<p><strong>在对于 YAML 解析的时候，错误处理不要太严格，小的错误不能影响 Skill 的解析，可以将错误信息返回给用户，以此进行警告提示</strong></p>
<p>🤔 那么解析之后的 SKILL.md 的元信息，是需要加载进入 Agent 的上下文中的，如何存储是一个值得考虑的问题</p>
<p>我们可以使用 Map 这种键值对的格式，<strong>将这些元信息存储在内存中</strong>，name 作为 key，value 值至少需要下面三个字段：</p>
<ul>
<li>name：名称</li>
<li>description：描述</li>
<li><strong>location：SKILL.md 文件的绝对路径</strong></li>
</ul>
<p>关于是否需要将 md 格式的正文内容也加载进来，有两种考虑，如果直接加载进来的话，内容存储会变大一些，但是在渐进式披露第二层的时候，读取速度会很快，如果不加载进来，那内存会小一些，等进入第二层上下文加载的时候，就要再读取这个 SKILL.md 文件，IO 读取会影响整个 Agent 的响应时间，这里需要自己根据实际业务进行权衡</p>
<h2>三、使用</h2>
<h3>3.1、将元信息放入到系统提示词中</h3>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/OtzQbJZakoj0TNxJrWAcdLKFnbb.CAEBZtHQ_Z1Ois7i.webp" alt="系统提示词放入元信息" /></p>
<ol>
<li>在 Agent 会话初始的时候，将元信息放入系统提示词中，<strong>放入的时候要提供一个简短的 Skill 使用说明，告诉模型如何使用以及什么时候使用</strong>，这个是第一层披露</li>
<li>当用户输入或任务触发了相应的 Skill 的时候，这个时候 Agent 会调用读取工具读取完整的 SKILL.md，这个是第二层披露</li>
<li>完整的 SKILL.md 加载进入 Agent 上下文中之后，遇到任务复杂度很高，Agent 需要得到更加详细的"说明文档"，这个时候会继续调用读取工具，从 SKILL.md 获取的引用路径读取相应的 references、assets、scripts</li>
<li>如果读取到 scripts 的时候，有需要执行脚本的需求，那么 Agent 就会调用 Bash 工具执行脚本文件</li>
</ol>
<p>在将元信息放入到系统提示词中的时候，采用一些特定的格式，XML、JSON 等，后续在上下文管理的时候会非常有效</p>
<pre><code>&lt;available_skills&gt;
  &lt;skill&gt;
    &lt;name&gt;code-review&lt;/name&gt;
    &lt;description&gt;Review code for bugs, style issues, and best practices. Use when the user wants feedback on their code.&lt;/description&gt;
    &lt;location&gt;/home/user/.agents/skills/code-review/SKILL.md&lt;/location&gt;
  &lt;/skill&gt;
  &lt;skill&gt;
    &lt;name&gt;git-commit&lt;/name&gt;
    &lt;description&gt;Generate clear and conventional git commit messages from diffs or change descriptions.&lt;/description&gt;
    &lt;location&gt;/home/user/project/.agents/skills/git-commit/SKILL.md&lt;/location&gt;
  &lt;/skill&gt;
&lt;/available_skills&gt;
</code></pre>
<p>location 字段表示的是 SKILL.md 的完整绝对路径，它的用途有两个：</p>
<ol>
<li>给读取工具提供正确的路径参数</li>
<li>为模型提供一个基本的路径参考，用于正确读取 SKILL.md 内容中的引用资源（reference、asset、scripts/xxx.js）</li>
</ol>
<p>🎃一点开发小建议：可以再提供一个文件列出的工具（listFiles），这样在第三层披露的时候，模型有工具可以"自修复"路径导致的读取错误</p>
<h3>3.2、将元信息放入到专有的工具中</h3>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/UhRmbzL4moEjr6xkx64cYcnOn9d.BBTH1WJp_Z1vIv1.webp" alt="专有工具使用 skill 元信息" /></p>
<ol>
<li>在会话开始的时候，Skill 的元信息会以工具的描述的格式提供给 Agent，这个是第一层渐进式披露</li>
<li>当 Agent 需要更完整的 SKILL.md 内容的说明，传入给工具的参数是 name，工具返回的是 SKILL.md 的内容，重点是对于引用资源的路径展示更为准确和具体了，这个是第二层渐进式披露</li>
<li>在第三层披露中，Agent 可以使用读取工具去获取完整的引用资源的内容，当然这里你也可以单独在创建一个工具获取引用资源的内容，也不一定仅依靠读取工具</li>
<li>Bash 工具依旧有效，可以执行 skill 中的 scripts 脚本文件</li>
</ol>
<p>使用专用工具和系统提示词这两种方式都是有效的，但是专用工具比系统提示词在<strong>开发控制</strong>上面更具有优势：</p>
<ul>
<li>控制返回的内容 - 只返回 SKILL.md 中的正文内容，而不是重复返回元信息</li>
<li>在上下文管理过程中，工具的返回结果可以单独做一些特殊的标记，在上下文压缩的时候是可以跳过筛选出来</li>
<li>引用资源是以结构化的列表形式展现，在模型理解上更加友好</li>
<li>可以做一些独特的控制流，工具是否执行等操作</li>
<li>可以做一些统计分析的功能</li>
</ul>
<pre><code>&lt;skill_content name="code-review"&gt;
# Code Review

## 何时使用
当用户需要对代码进行审查时使用此技能，包括 Bug 排查、
代码风格检查、最佳实践建议等。

## 审查步骤
1. 阅读代码，理解其意图
2. 对照 references/style-guide.md 检查风格问题
3. 运行 scripts/lint-check.sh 进行静态分析
4. 输出结构化的审查报告

Skill directory: /home/user/.agents/skills/code-review
此技能中的相对路径均相对于上述目录。

&lt;skill_resources&gt;
  &lt;file&gt;scripts/lint-check.sh&lt;/file&gt;
  &lt;file&gt;references/style-guide.md&lt;/file&gt;
  &lt;file&gt;references/common-bugs.md&lt;/file&gt;
&lt;/skill_resources&gt;
&lt;/skill_content&gt;
</code></pre>
<p>关于 <code>skill_resources</code> 的内容，之前是需要依靠 SKILL.md 中书写相应的引用介绍，但是如果是工具结果返回，我们可以单独将相应的 skill 下面的 reference、scripts、asset 文件夹都读一遍，然后将文件路径放入到 <code>skill_resources</code> 标签中，<strong>要注意限制文件列表大小，不要过大了，同时要向模型释放信号「当前的列表可能不完整，具体情况具体分析」，这样不会让模型在自主运行的过程中被工具返回内容限制</strong></p>
<h2>四、管理</h2>
<p><img src="https://wakeup-jin-blog.netlify.app/_astro/Kjujba3FOodXLnxZG4lcnF3YnUh.C8qourqO_2oQIMc.webp" alt="Skill 的管理" /></p>
<p>渐进式加载的意义是<strong>避免预加载所有 Skill</strong>，因为有些 Skill 刚开始可能不会使用到，加载进去是上下文的浪费</p>
<p><strong>但是已经加载进来的 Skill 内容，是值得在会话中持续保留的</strong>，因为 skill 已经成为了当前会话中 Agent 的一种任务行为指导，如果盲目的压缩，是会降低 Agent 的性能，保留 skill 的内容有两种方式</p>
<ol>
<li>系统提示词的使用：在读取工具的结果中，识别相应的结构化标签，来保留 skill 的内容</li>
<li>专用工具的使用：将技能激活工具的输出标记为保护状态，在压缩的时候进行标记判断</li>
</ol>
<p>但是这也要根据实际情况来看，如果我们的对话很长，skill 数量很多，上下文窗口小，不压缩的话，根本没有新的上下文空间给剩下的任务执行链，那么这个时候压缩是更优解</p>
<p><strong>但是压缩之后，要显示的提示模型"skill 也被压缩了，如有需要请重新加载相应的 skill"</strong></p>
<p>不然模型会陷入压缩幻觉，"觉得自己里面已经有了相应的 skill，不用在加载了"</p>
<p>还有一种更高级的用法：<strong>使用 subAgent（子智能体）运行 Skill</strong></p>
<p>子智能体的整个执行过程（技能指令 + 引用文件 + 中间推理）都发生在它的上下文窗口中，主智能体的上下文完全不受污染，只看最终结果</p>
<p>在开发的时候，我们可以将"执行 Skill"这个行为创建一个子智能体，将用户输入+Skill 元信息注入给子智能体，最终子智能体返回结果给主智能体消费</p>
<p>具体是否使用子智能体来执行 Skill，要交给主智能体自己来判断，例如：任务超过阈值的时候就自动委托</p>
<h2>五、快速构建</h2>
<p>上面提到的方式，是你从头给 Agent 构建一个支持 Skill 的功能模块，里面涉及到的元信息加载，正文内容读取，资源文件读取的功能，由你来创建一套读取工具或者读取 Skill 内容的工具</p>
<p>目前的生态比较成熟，很多 Agent SDK 是开箱即用这个 Skills 功能模块的，只要引入 SDK 下的 Agent 方法，那么 Agent 就自带 skills 加载的功能，接下来我给大家使用 Claude Agent SDK 构建一下看看，开发起来非常方便快速</p>
<p>1、Agent 运行文件</p>
<pre><code>"""简化版，主要展示核心流程的步骤，尤其是调用和输出结果解析"""
async def _run_repl_async() -&gt; None:
    config = load_runtime_config()
    apply_runtime_env(config)
    client = build_client(config)

    # 输入
    try:
        user_input = input("\n&gt; ").strip()
    except (EOFError, KeyboardInterrupt):
        return print("\n再见!")

    # 请求 + 接收
    try:
        print("[发送请求...]")
        await client.query(user_input)

        print("[接收响应...]")
        events = [e async for e in _iter_events(client)]
    except Exception as exc:
        print(f"[错误] {exc}")
</code></pre>
<p>2、Claude Agent SDK 核心运行配置文件</p>
<pre><code>"""SDK 封装 - 简化版"""
from __future__ import annotations

from pathlib import Path
from app.config import RuntimeConfig

# 允许 Agent 使用的工具列表
ALLOWED_TOOLS = ["Skill", "Read", "Write", "Edit", "Bash", "Grep", "Glob"]

def build_client(config: RuntimeConfig):
    """创建 Claude SDK 客户端"""
    try:
        from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
    except ImportError:
        raise RuntimeError(
            "请先安装 Claude Agent SDK: pip install claude-agent-sdk"
        )

    options = ClaudeAgentOptions(
        #设置 project 会默认加载项目内的./claude/skills 下的 skill
        setting_sources=["project", "user"],
        allowed_tools=ALLOWED_TOOLS,
        model="deepseek-chat",
        env={
            "ANTHROPIC_AUTH_TOKEN": config.api_key,
            "ANTHROPIC_BASE_URL": config.base_url,
        },
        # 单独指定 skills 文件加载的位置，配合 user 值使用
        add_dirs=["/xxx/xxxx/.agents/skills"]
    )
    return ClaudeSDKClient(options=options)
</code></pre>
<p>核心就是这两个文件，运行 Agent 之后，就会自动加载 skill 文件，同时也会自动使用渐进式披露的策略来进一步加载正文内容和资源文件</p>
<blockquote>
<p>[!TIP]
关于 Claude Agent SDK 模型最好是使用 Claude 的，但是因为一些因素无法使用的话，也可以考虑支持 ClaudeCode 和 Anthropic API 格式的模型供应商</p>
</blockquote>
<p>类似的 SDK 还有：</p>
<ul>
<li>pi-mono 中的 pi-coding-agent 核心包</li>
<li>kimi Agent SDK</li>
</ul>
<p>🪐 <strong>如果追求快速构建是完全可以使用这些成熟的 Agent SDK 构建 Skill 支持功能的，相应的你的 Agent 的整体构建设计思路也要去贴合这些 Agent SDK，我觉得是有利有弊的，需要开发者们按照场景去选择自己从头构建还是借助 SDK 集成，</strong></p>
<ul>
<li>自己构建自由度更高，可操作性更强，</li>
<li>使用 SDK 构建，速度更快，迭代方便，开发难度较小</li>
</ul>
]]></content:encoded>
            <author>WakeUp-Jin</author>
        </item>
    </channel>
</rss>