目标:学习 Claude Code 这类 coding agent 产品 的整体形态与关键原理。
方法:每学一个部分都用同一个模拟实例 串起来,把该部分的所有设计点尽可能映射到实例中的具体行为(包括但不限于:正常路径、各类边界情况、必要的兜底/纠错/恢复),让每个设计点在实例里"跑出来",从而理解它到底如何发挥作用、如何工作的。
宏观:Claude Code 作为"产品"到底是什么?
Claude Code 不是"更强的聊天",而是一个 CLI 里的编程执行系统 :你给一个目标,它会在你的代码仓库里 自主地读、改、跑、再改,直到达成目标,同时把风险动作控制在可接受范围内。
从产品视角,它给用户的体验通常是:
- 一句话目标:比如"修复这条测试失败、顺便把相关代码重构一下"
- 过程可见:它会展示正在读哪些文件、准备跑什么命令、打算改哪些片段
- 能执行:不是只建议,而是真的能改文件、跑测试、看输出、迭代
- 有护栏:危险命令/大范围修改会被拦、需要确认或被策略拒绝
- 长期可用:会话能续跑、能压缩上下文、能记住偏好、能复用技能
系统框架:它由哪些模块拼起来?(我们当前学的在什么位置)
可以把整个系统分成 7 个"层",每层解决一个产品级问题:
#mermaid-svg-NuPu3aH6kwE9Lwsk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NuPu3aH6kwE9Lwsk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NuPu3aH6kwE9Lwsk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NuPu3aH6kwE9Lwsk .error-icon{fill:#552222;}#mermaid-svg-NuPu3aH6kwE9Lwsk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NuPu3aH6kwE9Lwsk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NuPu3aH6kwE9Lwsk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NuPu3aH6kwE9Lwsk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NuPu3aH6kwE9Lwsk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NuPu3aH6kwE9Lwsk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NuPu3aH6kwE9Lwsk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NuPu3aH6kwE9Lwsk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NuPu3aH6kwE9Lwsk .marker.cross{stroke:#333333;}#mermaid-svg-NuPu3aH6kwE9Lwsk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NuPu3aH6kwE9Lwsk p{margin:0;}#mermaid-svg-NuPu3aH6kwE9Lwsk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NuPu3aH6kwE9Lwsk .cluster-label text{fill:#333;}#mermaid-svg-NuPu3aH6kwE9Lwsk .cluster-label span{color:#333;}#mermaid-svg-NuPu3aH6kwE9Lwsk .cluster-label span p{background-color:transparent;}#mermaid-svg-NuPu3aH6kwE9Lwsk .label text,#mermaid-svg-NuPu3aH6kwE9Lwsk span{fill:#333;color:#333;}#mermaid-svg-NuPu3aH6kwE9Lwsk .node rect,#mermaid-svg-NuPu3aH6kwE9Lwsk .node circle,#mermaid-svg-NuPu3aH6kwE9Lwsk .node ellipse,#mermaid-svg-NuPu3aH6kwE9Lwsk .node polygon,#mermaid-svg-NuPu3aH6kwE9Lwsk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NuPu3aH6kwE9Lwsk .rough-node .label text,#mermaid-svg-NuPu3aH6kwE9Lwsk .node .label text,#mermaid-svg-NuPu3aH6kwE9Lwsk .image-shape .label,#mermaid-svg-NuPu3aH6kwE9Lwsk .icon-shape .label{text-anchor:middle;}#mermaid-svg-NuPu3aH6kwE9Lwsk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NuPu3aH6kwE9Lwsk .rough-node .label,#mermaid-svg-NuPu3aH6kwE9Lwsk .node .label,#mermaid-svg-NuPu3aH6kwE9Lwsk .image-shape .label,#mermaid-svg-NuPu3aH6kwE9Lwsk .icon-shape .label{text-align:center;}#mermaid-svg-NuPu3aH6kwE9Lwsk .node.clickable{cursor:pointer;}#mermaid-svg-NuPu3aH6kwE9Lwsk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NuPu3aH6kwE9Lwsk .arrowheadPath{fill:#333333;}#mermaid-svg-NuPu3aH6kwE9Lwsk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NuPu3aH6kwE9Lwsk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NuPu3aH6kwE9Lwsk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NuPu3aH6kwE9Lwsk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NuPu3aH6kwE9Lwsk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NuPu3aH6kwE9Lwsk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NuPu3aH6kwE9Lwsk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NuPu3aH6kwE9Lwsk .cluster text{fill:#333;}#mermaid-svg-NuPu3aH6kwE9Lwsk .cluster span{color:#333;}#mermaid-svg-NuPu3aH6kwE9Lwsk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NuPu3aH6kwE9Lwsk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NuPu3aH6kwE9Lwsk rect.text{fill:none;stroke-width:0;}#mermaid-svg-NuPu3aH6kwE9Lwsk .icon-shape,#mermaid-svg-NuPu3aH6kwE9Lwsk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NuPu3aH6kwE9Lwsk .icon-shape p,#mermaid-svg-NuPu3aH6kwE9Lwsk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NuPu3aH6kwE9Lwsk .icon-shape .label rect,#mermaid-svg-NuPu3aH6kwE9Lwsk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NuPu3aH6kwE9Lwsk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NuPu3aH6kwE9Lwsk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NuPu3aH6kwE9Lwsk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户意图/目标
CLI/交互层
展示过程+确认+中断+续跑
Agent Loop 核心循环
模型决策→工具执行→结果回注→再决策
工具系统
读/改/跑/搜/网络/MCP
上下文工程
系统提示词+项目上下文+历史管理/压缩
权限与安全
规则/分类器/确认/沙箱/防越权
记忆与技能
跨会话偏好/可复用能力
多Agent/工作流
拆分任务/并行/隔离
全局结构图:15 部分 × 设计点(索引)
下面这张结构图的目的,是让你不翻全文也能从全局看到:每一部分主要解决什么、靠哪些设计点做到的;需要细节时,再回到对应章节的第 4 段"多轮实例"里看它怎么跑出来。
#mermaid-svg-nx1ICqmXO1XNbRsO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nx1ICqmXO1XNbRsO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nx1ICqmXO1XNbRsO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nx1ICqmXO1XNbRsO .error-icon{fill:#552222;}#mermaid-svg-nx1ICqmXO1XNbRsO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nx1ICqmXO1XNbRsO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nx1ICqmXO1XNbRsO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nx1ICqmXO1XNbRsO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nx1ICqmXO1XNbRsO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nx1ICqmXO1XNbRsO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nx1ICqmXO1XNbRsO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nx1ICqmXO1XNbRsO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nx1ICqmXO1XNbRsO .marker.cross{stroke:#333333;}#mermaid-svg-nx1ICqmXO1XNbRsO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nx1ICqmXO1XNbRsO p{margin:0;}#mermaid-svg-nx1ICqmXO1XNbRsO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nx1ICqmXO1XNbRsO .cluster-label text{fill:#333;}#mermaid-svg-nx1ICqmXO1XNbRsO .cluster-label span{color:#333;}#mermaid-svg-nx1ICqmXO1XNbRsO .cluster-label span p{background-color:transparent;}#mermaid-svg-nx1ICqmXO1XNbRsO .label text,#mermaid-svg-nx1ICqmXO1XNbRsO span{fill:#333;color:#333;}#mermaid-svg-nx1ICqmXO1XNbRsO .node rect,#mermaid-svg-nx1ICqmXO1XNbRsO .node circle,#mermaid-svg-nx1ICqmXO1XNbRsO .node ellipse,#mermaid-svg-nx1ICqmXO1XNbRsO .node polygon,#mermaid-svg-nx1ICqmXO1XNbRsO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nx1ICqmXO1XNbRsO .rough-node .label text,#mermaid-svg-nx1ICqmXO1XNbRsO .node .label text,#mermaid-svg-nx1ICqmXO1XNbRsO .image-shape .label,#mermaid-svg-nx1ICqmXO1XNbRsO .icon-shape .label{text-anchor:middle;}#mermaid-svg-nx1ICqmXO1XNbRsO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nx1ICqmXO1XNbRsO .rough-node .label,#mermaid-svg-nx1ICqmXO1XNbRsO .node .label,#mermaid-svg-nx1ICqmXO1XNbRsO .image-shape .label,#mermaid-svg-nx1ICqmXO1XNbRsO .icon-shape .label{text-align:center;}#mermaid-svg-nx1ICqmXO1XNbRsO .node.clickable{cursor:pointer;}#mermaid-svg-nx1ICqmXO1XNbRsO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nx1ICqmXO1XNbRsO .arrowheadPath{fill:#333333;}#mermaid-svg-nx1ICqmXO1XNbRsO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nx1ICqmXO1XNbRsO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nx1ICqmXO1XNbRsO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nx1ICqmXO1XNbRsO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nx1ICqmXO1XNbRsO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nx1ICqmXO1XNbRsO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nx1ICqmXO1XNbRsO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nx1ICqmXO1XNbRsO .cluster text{fill:#333;}#mermaid-svg-nx1ICqmXO1XNbRsO .cluster span{color:#333;}#mermaid-svg-nx1ICqmXO1XNbRsO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nx1ICqmXO1XNbRsO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nx1ICqmXO1XNbRsO rect.text{fill:none;stroke-width:0;}#mermaid-svg-nx1ICqmXO1XNbRsO .icon-shape,#mermaid-svg-nx1ICqmXO1XNbRsO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nx1ICqmXO1XNbRsO .icon-shape p,#mermaid-svg-nx1ICqmXO1XNbRsO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nx1ICqmXO1XNbRsO .icon-shape .label rect,#mermaid-svg-nx1ICqmXO1XNbRsO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nx1ICqmXO1XNbRsO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nx1ICqmXO1XNbRsO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nx1ICqmXO1XNbRsO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 可靠性层
自治与长期运行层
验收与运行层
任务分解与工作流层
记忆与复用层
速度与并发体验层
安全与治理层
上下文与提示词层
工具与执行层
核心循环层
入口与交互层
第七部分 CLI 与会话
-
One-shot vs REPL
-
flags->运行策略注入
-
messages 落盘持久化
-
resume 恢复会话
-
REPL 命令(/clear /compact /plan)
-
Ctrl+C 与预算上限收敛
第一部分 Agent Loop -
tool_use/tool_result 循环协议
-
state=messages 工作记忆
-
明确 continue/stop 条件
-
fail-as-data(错误回注)
-
验证纳入循环(build->test->fix)
第二部分 工具系统 -
工具契约:schema+executor
-
统一分发+可恢复错误
-
并发语义:只读并行/写入串行
-
编辑护栏:唯一+read-before-edit+mtime
-
风险治理:deny/confirm/allow
-
结果体积:落盘/预览/裁剪
第十二部分 MCP 集成 -
配置驱动声明 server(合并覆盖)
-
连接管理(spawn+握手+生命周期)
-
JSON-RPC over stdio
-
动态发现 tools + 前缀注册
-
tool_use->tools/call 透明路由
-
外部工具同样受治理(权限/并发/大结果)
第三部分 上下文工程 -
system 静态/动态分层(缓存友好)
-
system-reminder 注入项目规则
-
tools/schema 稳定性(含延迟加载)
-
大结果落盘引用(按需取回)
-
渐进式压缩(Budget/Snip/Micro/Auto)
-
turn boundary(结构正确性优先)
-
缓存感知门控(命中率 vs 可用性)
第六部分 System Prompt -
静态核心 + 动态环境分层
-
reminder 注入(不污染稳定前缀)
-
@include/rules 规则模块化
-
动态事实(cwd/git/status)
-
排序策略(近因效应)
-
与工具/权限/并发策略对齐
第四部分 权限与安全 -
统一权限闸门(checkPermission)
-
规则系统(deny 优先)
-
权限模式(default/plan/acceptEdits/yolo/dontAsk)
-
内置危险检测(detectors)
-
用户确认 + 会话白名单
-
拒绝可恢复(deny->tool_result)
第五部分 流式输出与并行早启动 -
token/delta 级文本流式渲染
-
双后端抽象(Anthropic/OpenAI 兼容)
-
tool_use 流式重建(chunk/事件)
-
只读工具 Early Exec(权限 allow 前置)
-
调度:只读并行/写入串行(分批)
-
thinking 展示不入历史(过滤)
第八部分 记忆系统 -
记忆落盘(每条一个文件+索引)
-
项目命名空间隔离
-
相关 Top-K 召回
-
注入位置(system vs user)
-
异步预取/sideQuery
-
与压缩/Resume 协作
第九部分 技能系统 -
技能即文件(frontmatter+模板)
-
发现与覆盖(user vs project)
-
模板变量/参数注入
-
注入 system(让模型可自动选择)
-
inline vs fork 两种执行
-
allowed-tools 安全边界
第十部分 Plan Mode -
只读强制(plan 权限模式)
-
enter/exit 状态切换工具
-
plan 工件落盘
-
审批工作流(4 选项)
-
模式可逆(恢复 prePlanMode)
-
deferred plan tools(延迟加载)
第十一部分 多 Agent -
agent 工具(外包接口)
-
子 agent 独立上下文隔离
-
工具白名单(只读子 agent)
-
类型化(explore/plan/general)
-
fork-return(返回摘要降噪)
-
与权限/上下文/流式一致协作
第十三部分 功能测试与验收 -
手动场景验收(现象/观察点)
-
自动化集成测试(mock+子进程)
-
双后端覆盖(Anthropic/OpenAI)
-
双实现覆盖(TS/Python)
-
权限策略分层(yolo/auto)
-
live 冒烟 vs mock 回归
第十四部分 自治与续跑 -
goal 独立评估器
-
reason reinjection 回灌
-
刹车(impossible/上限/budget)
-
loop(interval vs dynamic)
-
作用域工具(schedule_wakeup)
-
Auto Mode 分类器替确认框
-
脱敏 + 强契约 + fail-closed
-
拒绝上限与回退
第十五部分 错误恢复与可靠性 -
fail-as-data(失败回注)
-
退避重试(withRetry)
-
取消/中断(Abort/Ctrl+C)
-
turn boundary(结构正确性)
-
compact->retry(上下文压力恢复)
-
fail-closed 工具护栏
-
外部依赖降级路径
-
预算与上限(避免无限重试/循环)
我们当前学的 "第一部分:Agent Loop(核心循环)" 就是中间那块 引擎。它不负责具体"怎么读文件",也不负责"怎么显示 UI",但它决定了产品是不是从"聊天"变成"做事"。
第一部分:Agent Loop(核心循环)
1.【Agent Loop】在产品链里的位置
在产品链路里,Agent Loop 位于"用户意图"与"工具执行"之间,是整个系统的引擎/编排器:
- 上游:CLI/UX 把用户目标送进来
- 中游:Agent Loop 调用模型、解析 tool_use、把工具结果回注、决定是否继续
- 下游:工具系统真正去读/改/跑,并把事实返回
一句话:模型决定"下一步要做什么",Agent Loop 负责让它真的发生,并把事实带回下一轮。
2.【Agent Loop】要解决产品的什么问题
- 把"建议"变成"执行":不只是回答,而是能在仓库里完成多步任务
- 把复杂任务拆成多轮可推进的闭环:读→改→跑→再改→直到完成
- 让系统具备"自我修正"能力:失败/不确定不是终点,而是下一轮的输入
- 让流程通用而不写死:不靠 if/else 写死工作流,靠模型在每轮基于事实决策
3.【Agent Loop】是通过几类什么设计,分别如何解决的问题
下面把 Agent Loop 的设计点分成 5 类(后面的实例会按顺序把它们全部跑出来):
- 【设计 A:工具感知的循环协议(tool_use/tool_result)】
- 怎么做:每轮调用模型时带上 tools;若回复包含 tool_use,则执行工具并把 tool_result 回注到消息历史,再进入下一轮。
- 解决:让模型从"只能说"变成"能动手"。
- 【设计 B:消息历史作为工作记忆(state = messages)】
- 怎么做:每轮都把 user/assistant/tool_result 追加进 messages;下一轮模型能看到"我刚做了什么 + 工具返回了什么"。
- 解决:让多轮任务可持续推进,不靠外部硬编码状态机。
- 【设计 C:明确的继续/停止条件】
- 怎么做:当某轮回复中"不再请求工具"(无 tool_use)即认为本轮收敛并停止;否则继续。
- 解决:让系统在"什么时候结束"这件事上有确定边界。
- 【设计 D:错误作为数据回注(fail as tool_result)】
- 怎么做:工具失败/未知工具/定位不唯一等都不抛崩溃,而是生成可读 tool_result 回注;模型基于反馈调整下一步。
- 解决:把失败转化为"可恢复的下一轮输入",形成自愈闭环。
- 【设计 E:把验证纳入循环(build→test→fix)】
- 怎么做:模型会主动调用 run_shell 跑测试;失败输出进入 tool_result;下一轮据此修正。
- 解决:让结果可靠,而不是"一次性看起来对"。
4.【实例】用一个实例多轮跑,完整覆盖展示每个设计在实例中如何工作
实例任务(贯穿全书):
"修复单元测试失败:
test_total_should_round。修好后跑全量测试,最后给我一个简短总结。"
下面是一个流畅的多轮"运行故事"。每个关键节点都会标注它对应的设计点(A--E)。
Turn 1:先建立事实(搜索定位)
assistant:"我先定位失败测试和实现位置。"
tool_use:
grep_search({ pattern: "test_total_should_round", path: "." })tool_result: "tests/test_total.py:42: ..."
tool_use:
grep_search({ pattern: "def total", path: "src" })tool_result: "src/calc.py:10: ..."
【设计 A:工具感知循环协议】模型请求工具 → 系统执行 → tool_result 回注 → 继续下一轮。
【设计 B:消息历史是工作记忆】这轮得到的"事实"被写进 messages,下一轮会直接用。
Turn 2:读文件拿到真实上下文
tool_use:
read_file({ file_path: "tests/test_total.py" })tool_result: "期望 round 到 2 位..."
tool_use:
read_file({ file_path: "src/calc.py" })tool_result: "实现里有截断/浮点误差风险..."
【设计 B:消息历史是工作记忆】模型不靠猜测,而是把文件事实变成工作记忆。
Turn 3:第一次尝试修改(出现定位不唯一)
tool_use:
edit_file({ file_path: "src/calc.py", old_string: "round(x, 2)", new_string: "..." })tool_result: "Error: old_string found 2 times. Must be unique."
【设计 D:错误作为数据回注】工具不"随便改一个",而是把失败变成可行动反馈,让模型下一轮修正定位。
【设计 A:工具感知循环协议】失败也一样进入 tool_result,循环继续。
Turn 4:自我修正(扩大上下文→精准编辑)
tool_use:
read_file({ file_path: "src/calc.py" })(扩大上下文)tool_use:
edit_file({ file_path: "src/calc.py", old_string: "<更长且唯一的片段>", new_string: "<替换后的片段>" })tool_result: "Successfully edited src/calc.py"
【设计 D:错误驱动修正】上一轮失败的反馈,直接决定了这一轮"先 re-read 再精确替换"。
Turn 5:把验证纳入闭环(跑测试→基于失败输出再修)
tool_use:
run_shell({ command: "pytest -q" })tool_result: "FAIL: 仍差 0.01,堆栈指向浮点误差..."
tool_use:
edit_file(...)tool_result: "Successfully edited..."
tool_use:
run_shell({ command: "pytest -q" })tool_result: "PASSED"
【设计 E:验证纳入循环】测试失败输出成为下一轮修改的事实依据。
【设计 D:错误作为数据回注】失败不是"终止",是"下一轮输入"。
【设计 A:工具协议】每次 tool_use 都有 tool_result,循环按事实推进。
Turn 6:自然停止(没有 tool_use)
assistant(纯文本):"测试通过。根因是 ... 我做了 ... 影响范围 ..."
【设计 C:停止条件】当模型不再请求工具(无 tool_use),循环自然停止。
5.【Agent Loop】总结
- 最核心一句话:Agent Loop = "调用模型 → 工具执行 → 结果回注 → 继续/停止"。
- 它解决的关键产品问题:把聊天变成可执行闭环,把验证纳入流程,并用错误回注实现自我修正。
- 你应带走的记忆点:A 工具协议、B 消息历史=状态、C 停止条件、D 错误=数据、E 验证闭环。
第二部分:工具系统(手脚与治理)
1.【工具系统】在产品链里的位置
工具系统位于 Agent Loop 的下游,是产品"触达真实世界"的那一层:
- 上游:Agent Loop 决定要做什么、何时做、做完是否继续
- 下游:工具系统真正执行(读/改/搜/跑/网),并把结果变成可回注的事实
一句话:工具系统是手脚,也是治理入口。
2.【工具系统】要解决产品的什么问题
- 把自由意图收敛成可执行动作(否则模型只能输出建议)
- 让执行可靠可控(参数稳定、定位准确、并发可控、风险可控、输出可控)
- 让能力可扩展(新增能力=新增工具)
3.【工具系统】是通过几类什么设计,分别如何解决的问题
我们把工具系统归纳成 6 类设计点:
- 【设计 1:工具契约(name/description/schema/executor)】:用 schema 约束输入形状、用 executor 实现动作 → 调用稳定、可扩展。
- 【设计 2:统一分发与可恢复错误】:Unknown tool/参数不合法不崩溃,返回可读结果 → 模型自我纠正、会话不断。
- 【设计 3:并发语义】:只读工具可并行、写工具串行 → 体感更快 + 避免竞态覆盖。
- 【设计 4:编辑防误改】:唯一匹配 + read-before-edit + mtime 冲突检测 → 宁可失败也不误改,支持人机协作。
- 【设计 5:风险治理入口】:deny/confirm/allow 统一判定 → 敢执行的前提。
- 【设计 6:结果体积治理】:落盘引用/预览/必要时裁剪 → 不撑爆上下文(与第三部分强耦合)。
4.【实例】用一个实例多轮跑,完整覆盖展示每个设计在实例中如何工作
仍用同一个任务:
"修复单元测试失败:
test_total_should_round。修好后跑全量测试,最后给我一个简短总结。"
下面是一段连续的运行故事(把设计 1--6 都串进来)。
Turn 1:理解阶段(搜索/读文件)
tool_use:
grep_search({ pattern: "test_total_should_round", path: "." })tool_result: "tests/test_total.py:42: ..."
【设计 1:工具契约】pattern/path 这种稳定参数结构,让模型"会用工具"。
【设计 3:并发语义】真实产品会把多个只读工具并行启动(搜 + 读多个文件),加速理解阶段。
Turn 2:第一次编辑(定位不唯一→拒绝)
tool_use:
edit_file({ file_path: "src/calc.py", old_string: "round(x, 2)", new_string: "..." })tool_result: "Error: old_string found 2 times. Must be unique."
【设计 4:编辑防误改(唯一匹配)】拒绝"猜一个地方改",避免误改。
【设计 2:可恢复错误】失败不是崩溃,而是下一轮的明确反馈。
Turn 3:补上下文再改(read-before-edit / mtime)
tool_use:
read_file({ file_path: "src/calc.py" })tool_use:
edit_file({ file_path: "src/calc.py", old_string: "<更长且唯一的片段>", new_string: "<替换后的片段>" })
【设计 4:read-before-edit】编辑必须基于读过的事实。
【设计 4:mtime 冲突检测】如果你在 IDE 同时改过文件,会收到"请重读"的提示,避免覆盖。
Turn 4:验证(跑测试 + 大输出治理)
tool_use:
run_shell({ command: "pytest -q" })tool_result: "Output too large... saved to .../tool-results/... + preview"
【设计 6:结果体积治理】大日志落盘引用,messages 里只放预览与路径。
【设计 1:工具契约】run_shell 的输入/输出形状稳定,便于下一轮继续决策。
Turn 5:出现危险动作时(治理入口)
tool_use:
run_shell({ command: "rm -rf .pytest_cache" })tool_result: "Denied/Need confirmation: destructive command"
【设计 5:风险治理入口】危险命令不会直接执行;模型会换方案或请求确认。
Turn 6:完成(输出总结)
模型在多轮"读→改→跑→再改"后,不再请求工具,输出最终总结。
5.【工具系统】总结
- 工具系统既是"能力面"(能做什么),也是"治理面"(能否安全可靠地做)。
- 你应带走的记忆点:设计 1 契约、2 可恢复错误、3 并发语义、4 编辑防误改、5 风险治理、6 结果体积治理。
第三部分:上下文工程(Context Engineering / Context Management)
1.【上下文工程】在产品链里的位置
上下文工程位于 Agent Loop 的"每次调用模型之前/之后":
- 调用前:组装 system/tools/messages,并在必要时对历史做压缩/裁剪/落盘引用
- 调用后:把 assistant 输出与 tool_result 追加进历史(历史持续膨胀)
一句话:它是 Agent Loop 的"办公桌管理系统"。
2.【上下文工程】要解决产品的什么问题
- 上下文窗口是硬上限 :会话越长、工具输出越多,迟早
prompt too long - 决策质量取决于"看到了什么":上下文组织差会导致忘记、重复读、矛盾、跑偏
- 还要快/省:前缀缓存要求前缀字节稳定,逼迫系统"带着镣铐跳舞"
3.【上下文工程】是通过几类什么设计,分别如何解决的问题
我们把这一部分归纳为 7 类设计点(后面的实例会全部跑出来):
- 【设计 1:System Prompt 静态/动态分层】:稳定前缀尽量不变 → 更快更省(缓存友好)。
- 【设计 2:项目规则用 system-reminder 注入】 :
CLAUDE.md/日期等项目差异信息不污染最稳定前缀 → 缓存命中更好。 - 【设计 3:tools/schema 尽量稳定】:工具数组顺序/内容尽量稳定(必要时延迟加载/增量注入)→ 少破缓存。
- 【设计 4:大结果可恢复引用(落盘 + 预览 + 路径)】:长日志/大输出不塞满 messages → 按需 read 取回细节。
- 【设计 5:渐进式压缩流水线(轻→重)】:Budget/Snip/Microcompact 先顶住增长,逼近上限再 Auto-compact 摘要。
- 【设计 6:结构正确性优先(turn boundary 才能重写历史)】 :避免破坏
tool_use ↔ tool_result配对与 API 结构合法性。 - 【设计 7:缓存感知的门控权衡】:缓存热且压力不大时尽量不改历史;压力逼近上限时宁可破缓存也要腾空间。
4.【实例】用一个实例多轮跑,完整覆盖展示每个设计在实例中如何工作
实例任务(拉长一点,让上下文压力真实出现):
"修复单元测试失败:
test_total_should_round。修好后跑全量测试。然后做一次小重构(提取函数/清理命名),最后给我简短总结。"
下面是一个"流畅的会话故事",你会看到上下文如何长大、系统何时出手、每次出手对应哪个设计点。
Turn 0:会话初始化(先把稳定的放在稳定位置)
- 系统构造
system:核心规则尽量稳定,动态信息放后面。
【设计 1:静态/动态分层】 - 系统发现仓库规则(如
CLAUDE.md)与日期,将其包装成<system-reminder>注入到第一条 user(或附件)而非污染 system 核心。
【设计 2:system-reminder 注入】 - tools/schema 准备好并保持顺序稳定。
【设计 3:tools 稳定性】
Turn 1:读/搜建立事实(历史开始膨胀)
tool_use:
grep_search(...)/read_file(...)(若干)tool_result:(若干)
工具结果被追加进 messages,成为模型下一轮的工作记忆。历史增长是必然的,这也为后续压缩管道埋下"压力源"。
Turn 2:跑测试(长输出 → 先落盘引用)
tool_use:
run_shell({ command: "pytest -q" })tool_result:
[Output too large... saved to .../tool-results/...] + preview
系统把全文放到磁盘,只把预览+路径放进 messages;模型需要细节时再 read_file(<saved_log_path>)。
【设计 4:落盘引用 + 按需取回】
Turn 3:长任务态(轻量压缩先顶住)
下一次模型调用前,系统先跑轻量级压缩流水线:
- 旧 tool_result 先变短(保留头尾)
【设计 5:Budget】 - 重复/过时结果替换成占位符(保留"我读过哪个文件"的元信息)
【设计 5:Snip】 - 条件满足时更激进清理旧结果,只保留最近几条
【设计 5:Microcompact】
你会在行为上看到:模型更频繁地 re-read 关键文件/日志,这不是退化,而是"按需取回"的健康节奏。
Turn 4:结构正确性(为什么重写历史要卡边界)
在 tool_use/tool_result 配对严格的协议下,系统不会在工具循环中段随意重写历史,而是把"重写历史"的动作放在安全边界(turn boundary)执行。
【设计 6:结构正确性优先】
Turn 5:逼近上限(最重手段:Auto-compact 摘要替换)
轻量手段不够时,系统会做一次 summarizer 调用,把长历史压成 summary,并重建 messages 为"摘要 + 最近若干条"。
【设计 5:Auto-compact(最后手段)】
行为上你会看到:会话不断;但早期细节变粗,需要时模型会用工具再取回(read/grep)。
Turn 6:缓存张力(为什么有时忍着不压缩,有时突然压很狠)
- 缓存仍热、窗口压力不大:尽量不改写旧前缀(保命中,快/省)
【设计 7:缓存感知门控】 - 压力逼近上限:宁可破缓存也要腾空间(保可用性)
【设计 7:可用性优先】
5.【上下文工程】总结
- 上下文工程让系统在"窗口有限"的现实里仍能长期推进任务,同时兼顾快/省/稳。
- 你应带走的记忆点:设计 1--3(稳定性/缓存友好)、设计 4(落盘引用按需取回)、设计 5(渐进式压缩流水线)、设计 6(结构正确性边界)、设计 7(缓存张力权衡)。
第四部分:权限与安全(Permissions & Safety)
1.【权限与安全】在产品链里的位置
权限与安全位于 "模型请求工具调用" → "工具真正执行" 之间,是工具系统的统一闸门:
- 上游:Agent Loop 解析到
tool_use(模型想让系统执行一个动作) - 中游:权限系统判断
allow / deny / confirm(必要时与用户交互) - 下游:只有被允许的调用才会进入工具 executor 真执行;被拒绝的调用会立刻返回
tool_result(但不会执行)
一句话:它是 coding agent 的"刹车系统"------没有它,工具越强,事故越大。
2.【权限与安全】要解决产品的什么问题
- 把"能执行"变成"敢执行" :Agent 一旦能
run_shell、能写文件,就必然会碰到rm、git push、覆盖文件等高风险动作。 - 把"风险处理"产品化:不能靠提示词"不要做危险事",而要靠系统层的可执行闸门(默认拒绝/确认)。
- 让团队可控可配置:不同团队的风险容忍度不同,需要 allow/deny 规则与模式(例如 CI 环境不弹窗)。
- 让被拒绝也能继续推进 :拒绝不是崩溃,应该变成可读的
tool_result,让模型调整策略继续完成任务。
3.【权限与安全】是通过几类什么设计,分别如何解决的问题
我们把权限与安全归纳成 6 类设计点(后面的实例会全部跑出来):
- 【设计 1:统一权限闸门(checkPermission)】
- 怎么做 :每个
tool_use在执行前先判定{action: allow|deny|confirm, message?}。 - 解决:把"风险判断"从各工具里抽成统一入口,保证一致性与可审计性。
- 怎么做 :每个
- 【设计 2:规则系统(allow/deny,deny 优先)】
- 怎么做 :支持配置
allow与deny;永远先查deny再查allow。 - 解决 :团队能"先放开再收紧",并且 deny 能约束一切模式 (包括
--yolo)。
- 怎么做 :支持配置
- 【设计 3:权限模式(default / plan / acceptEdits / bypassPermissions / dontAsk)】
- 怎么做 :用模式表达"全局策略":
default:危险动作需要确认plan:只读契约(写入与 shell 直接拒绝)acceptEdits:编辑类工具自动放行bypassPermissions(--yolo):在规则/plan 检查后其余放行dontAsk:需要确认的一律自动拒绝(适合 CI)
- 解决:让"同一套工具能力"在不同场景下有不同风险策略。
- 怎么做 :用模式表达"全局策略":
- 【设计 4:内置危险检测(built-in detectors)】
- 怎么做 :对
run_shell的危险命令(如rm -rf、git push、sudo等)触发confirm;对"新建文件/编辑不存在文件"等也可触发确认。 - 解决:即使没有团队自定义规则,也能覆盖最常见事故。
- 怎么做 :对
- 【设计 5:用户确认 + 会话级白名单(confirmed set)】
- 怎么做:当 action=confirm 时弹确认;同一条危险动作在同一会话确认过一次就加入白名单,后续不再重复询问。
- 解决:既安全,又不把用户烦死(把"重复确认"变成"只确认一次")。
- 【设计 6:拒绝也要可恢复(deny → tool_result)】
- 怎么做 :拒绝时不抛异常中断,而是把"Denied/User denied/Auto-denied"作为
tool_result回注。 - 解决:模型能据此改策略(换命令、改成只读动作、请求用户明确授权),让任务继续。
- 怎么做 :拒绝时不抛异常中断,而是把"Denied/User denied/Auto-denied"作为
4.【实例】通过一个实例多轮跑,完整覆盖展示每个设计在实例中具体是如何工作的
仍然用同一个任务(延续前面三部分):
"修复单元测试失败:
test_total_should_round。修好后跑全量测试。然后做一次小重构(提取函数/清理命名),最后给我简短总结。"
Turn 0:会话启动时就决定"全局策略"(权限模式 + 规则)
你启动 agent 时,系统就确定了两件关键背景(后面每个 tool_use 都会走它们):
- 权限模式 :例如默认
default(危险动作需要确认)。
【设计 3:权限模式】 - 规则来源 :例如项目里配置了 deny:
run_shell(git push*)(团队不允许自动 push)。
【设计 2:规则系统(deny 优先)】
Turn 1:安全动作自动放行(不打扰用户)
模型需要跑测试验证修复:
tool_use:
run_shell({ command: "pytest -q" })tool_result: "FAIL ...(正常测试输出)"
这类命令不命中危险检测,也不命中 deny 规则,于是直接 allow 并执行。
【设计 1:统一闸门】每次都先判定再执行,但"安全路径"要做到低摩擦。
【设计 4:内置危险检测】没触发危险模式 → 不需要确认。
Turn 2:危险动作触发 confirm(用户拒绝 → 模型换策略)
模型想清理缓存,直接给出:
tool_use:
run_shell({ command: "rm -rf .pytest_cache" })tool_result: "Needs confirmation: rm -rf .pytest_cache"
系统识别到 rm -rf 属于危险命令,于是返回 confirm(此时不执行 )。
【设计 4:内置危险检测】把危险动作抬到"需要确认"的分支。
【设计 1:统一闸门】确认发生在执行前,避免"先执行后追责"。
你点了"否"(拒绝):
tool_result: "User denied this action."
【设计 6:拒绝可恢复】拒绝被回注为 tool_result,循环继续,模型必须调整策略。
模型下一轮可能换成更安全的等价动作:
tool_use:
run_shell({ command: "pytest -q --cache-clear" })tool_result: "FAIL ...(输出)"
Turn 3:用户确认一次后进入白名单(同一动作不再重复问)
你决定允许一次清理缓存(比如你手动确认它只删 .pytest_cache):
tool_use:
run_shell({ command: "rm -rf .pytest_cache" })(弹窗)你点"是"
tool_result: "OK"
系统把该确认信息加入会话白名单,后续同样的确认消息不会再弹。
【设计 5:确认 + 会话白名单】"一次确认,多次复用"。
紧接着模型又想再跑一次同样动作(例如重试流程里重复触发),这次:
tool_use:
run_shell({ command: "rm -rf .pytest_cache" })tool_result: "OK"(无弹窗)
Turn 4:deny 规则优先(连 --yolo 也拦得住)
修复完成后,模型想把改动推到远端:
tool_use:
run_shell({ command: "git push origin main" })tool_result: "Action denied: Denied by permission rule for run_shell"
这里不弹确认 ,因为规则是明确的团队策略:直接拒绝。
【设计 2:deny 优先】deny 命中立即拒绝。
【设计 6:拒绝可恢复】模型会改策略:改为提示你"我不能 push,但可以给你命令/PR 建议"。
现在你为了加速,重启会话时加了 --yolo(bypassPermissions),并 --resume 接着同一任务跑:
tool_use:
run_shell({ command: "git push origin main" })tool_result: "Action denied: Denied by permission rule for run_shell"
仍然被拦下。
【设计 3:bypassPermissions 的边界】它不是"绕过一切",仍受 deny 规则约束。
【设计 2:deny 对所有模式生效】这就是"敢给 yolo"的前提。
Turn 5:dontAsk(CI)模式下,confirm 变成自动拒绝
你在 CI/无人值守环境用 --dont-ask(同样 --resume 接着任务),模型试图做一个需要确认的动作(比如写一个不存在的新文件):
tool_use:
write_file({ file_path: "notes/debug.md", content: "..." })tool_result: "Action denied: Auto-denied (dontAsk mode): write new file: notes/debug.md"
【设计 3:dontAsk】无人值守场景不能弹窗,系统选择 fail-closed:需要确认的一律拒绝。
【设计 6:拒绝可恢复】模型会改策略:改为"把内容输出在聊天里"或"只修改已存在文件"。
5.【权限与安全】总结
- 权限系统的本质是:让工具能力在真实机器上可控可用。
- 你应带走的记忆点:
- 先闸门再执行(设计 1)
- deny 优先且能约束一切模式(设计 2)
- 用模式把场景产品化(设计 3)
- 危险检测 + 确认 + 白名单 = 低摩擦安全(设计 4/5)
- 拒绝要回注成 tool_result,推动自愈(设计 6)
第五部分:流式输出(Streaming)与并行早启动
1.【流式输出】在产品链里的位置
流式输出发生在 Agent Loop 的"调用模型"这一步内部,是用户体验层最显著的一层:
- 上游:Agent Loop 组装 system/tools/messages,发起一次模型调用
- 中游:模型响应不是一次性返回,而是以"增量事件流"的形式到达(text delta / tool block delta)
- 下游:UI/CLI 把增量文本立刻打印;系统在合适时机把"已完整的 tool_use"提前分发执行(早启动)
一句话:Streaming 把"等待"变成"进行中",并把工具延迟藏进模型生成窗口里。
2.【流式输出】要解决产品的什么问题
- 体验问题:大段输出的"黑屏等待":模型想 5--30 秒再一次性吐完,用户会觉得卡。
- 效率问题:工具延迟暴露:如果必须等模型完整输出后才执行工具,读文件/搜代码的 1--2 秒会被用户实打实感知到。
- 兼容性问题:不同后端的流式协议不同:Anthropic SDK 原生 stream;OpenAI 兼容接口需要手动拼装 chunks(尤其是 tool_calls)。
3.【流式输出】是通过几类什么设计,分别如何解决的问题
我们把本部分归纳成 6 类设计点(后面的实例会全部跑出来):
- 【设计 1:文本流式渲染(token/delta 级输出)】
- 怎么做:模型每产生一小段 text delta,就立即打印到终端。
- 解决:用户不再"黑屏等结束",而是持续看到进度。
- 【设计 2:后端抽象(Anthropic / OpenAI 兼容双后端)】
- 怎么做:同一套 Agent Loop 逻辑,上游只关心"拿到增量 + 拿到 final message";底层分别适配不同流协议。
- 解决:换模型/换 base URL 不改核心逻辑。
- 【设计 3:tool_use 的流式重建(chunk 累积/事件跟踪)】
- 怎么做 :流式过程中要跟踪工具 block:累积 input_json_delta,直到 block 完整(例如
content_block_stop)才能得到可执行的 JSON。 - 解决:让"工具调用"也能在 streaming 下可靠解析,而不是半截 JSON 就误执行。
- 怎么做 :流式过程中要跟踪工具 block:累积 input_json_delta,直到 block 完整(例如
- 【设计 4:安全前提下的工具早启动(Early Exec)】
- 怎么做 :当某个
tool_useblock 在流式中完整到达时,如果工具是并发安全的只读工具,并且权限检查allow,就立即开始执行,不必等整个模型响应结束。 - 解决:把工具执行时间藏进模型生成时间里,体感更快。
- 怎么做 :当某个
- 【设计 5:只读并行 + 写入串行的调度策略】
- 怎么做 :Anthropic 后端早启动天然实现并行;OpenAI 后端在响应结束后把"连续的安全工具"分批
Promise.all/asyncio.gather并行执行。 - 解决:读多文件时 2--3x 加速,同时不越过写操作引入竞态。
- 怎么做 :Anthropic 后端早启动天然实现并行;OpenAI 后端在响应结束后把"连续的安全工具"分批
- 【设计 6:无效高成本内容过滤(thinking 不入历史)】
- 怎么做:如果后端返回 thinking blocks,把它们用于 UI 展示但不写入 messages 历史。
- 解决:避免上下文窗口被"对后续决策没帮助"的大块内容占满(与第三部分上下文工程一致)。
4.【实例】通过一个实例多轮跑,完整覆盖展示每个设计在实例中具体是如何工作的
仍然用同一个任务:
"修复单元测试失败:
test_total_should_round。修好后跑全量测试。然后做一次小重构(提取函数/清理命名),最后给我简短总结。"
我们把重点放在"你在终端里真实看到的执行长相"。
Turn 1:文本先到(你先看到"我在做什么",而不是等工具结果)
模型开始生成回复时,不等整段生成完,终端就开始逐字出现:
assistant(流式逐字出现):"我先定位失败测试与实现位置,然后读相关文件......"
【设计 1:文本流式渲染】你会先看到它的意图与步骤,体感上"它在干活了"。
Turn 2:第一个 tool_use block 完整就立刻早启动(读工具把延迟藏起来)
模型在同一轮回复里先后请求多个只读工具:
tool_use:
grep_search({ pattern: "test_total_should_round", path: "." })tool_use:
read_file({ file_path: "tests/test_total.py" })tool_use:
read_file({ file_path: "src/calc.py" })
在 Anthropic 流式里,这些 tool_use 会以 block 形式到达:当某个 tool_use 的 input JSON 完整接收(content_block_stop),系统就立刻:
- 先做权限检查(必须 allow)
- 再对"并发安全工具"启动执行(early exec)
【设计 3:tool_use 流式重建】只有 block 完整才解析 JSON 并执行。
【设计 4:工具早启动】第一个 read/grep 往往在模型还在输出后续文字时就开始跑了。
【设计 5:只读并行】多个只读工具会自然重叠执行,你在下一步看到 tool_result 时常常是"几乎同时到齐"。
Turn 3:同一轮里出现写操作时,调度自动收敛为串行(不跨越写操作并行)
模型发现问题后要编辑文件:
tool_use:
edit_file({ ... })
这类写入工具不会被当作"并发安全早启动"去乱并行:它要么等到适当时机串行执行,要么至少保证不与其他写操作并发。
【设计 5:写入串行】保证不发生竞态覆盖(与第二部分工具系统一致)。
Turn 4:换后端时,表现一致但实现不同(OpenAI 兼容的 chunk 累积)
你把后端换成 OpenAI 兼容接口时,仍然能看到"边输出边显示":
assistant(delta.content 持续出现)......
但工具调用(tool_calls)在流式里是"分 chunk 到达"的,系统必须按 index 把 arguments 片段拼起来,直到完整后才执行。
【设计 2:双后端抽象】上层行为一致。
【设计 3:chunk 累积重建】保证 tool_calls 不会半截就执行。
同时,因为 OpenAI 兼容后端通常没有"tool block stop"事件用于早启动,系统会在响应结束后把连续的只读工具分批并行执行:
【设计 5:批量并行】[read, read, write, read] 会被分为 [read||read]、[write]、[read]。
Turn 5:thinking 很长,但历史很干净(不把无效成本塞进上下文)
当你开启 thinking/深度推理时,终端可能会展示更长的 thinking,但写入到 messages 历史的 content 会过滤掉 thinking blocks:
【设计 6:thinking 不入历史】避免后续回合上下文被无效内容撑爆。
5.【流式输出】总结
- Streaming 解决的是体感与吞吐:让你"看到进度",并把工具延迟藏进模型生成窗口里。
- 你应带走的记忆点:
- 先看到文本进度(设计 1)
- 后端差异被抽象隔离(设计 2/3)
- 早启动=只读并发 + 权限 allow 前置(设计 4/5)
- thinking 不入历史,和上下文工程联动(设计 6)
第六部分:System Prompt(提示词工程与规则注入)
1.【System Prompt】在产品链里的位置
System Prompt 位于 Agent Loop 的"每次调用模型之前"的最前置位置,是一次请求里最稳定、权重最高的一段上下文:
- 上游:用户目标进入会话(同一句话在不同仓库也会有不同规则)
- 中游:系统构造 system prompt(静态核心 + 动态环境),并把项目规则/日期等以
<system-reminder>注入到消息侧 - 下游:模型在每一轮决策"先读后改/是否要确认危险动作/如何用工具"等时,都以它为行为底座
一句话:工具给能力,权限给刹车,上下文给记忆;System Prompt 给"行为宪法"。
2.【System Prompt】要解决产品的什么问题
- 模型需要"身份 + 行为边界":否则它会在不同回合漂移(该谨慎时不谨慎、该先读时直接改)。
- 同一句用户指令在不同仓库要遵守不同规则 :例如项目要求、目录结构、团队规范(
CLAUDE.md/ rules)。 - 还要兼顾缓存/成本/速度:稳定的 system 前缀越稳定越好(第三部分已讲前缀缓存张力)。
- 把"产品策略"变成可维护的工程资产:规则要可 include、可组合、可分层,而不是一坨硬编码字符串。
3.【System Prompt】是通过几类什么设计,分别如何解决的问题
我们把 System Prompt 工程归纳成 6 类设计点(后面的实例会全部跑出来):
-
【设计 1:静态核心 + 动态环境分层】
- 怎么做:静态核心写"身份/总规则/工具偏好",跨会话字节不变;动态块拼"cwd/platform/shell/git/技能/记忆"等。
- 解决:行为稳定 + 缓存友好 + 环境事实可更新。
-
【设计 2:项目规则不污染最稳定前缀(system-reminder 注入)】
- 怎么做 :把
CLAUDE.md、日期等项目差异信息包装成<system-reminder>,注入到第一条 user(或附件消息)而不是塞进 system 核心。 - 解决:不同仓库差异化生效,同时不破坏静态前缀稳定性。
- 怎么做 :把
-
【设计 3:规则可组合(@include / rules 目录)】
- 怎么做 :
CLAUDE.md支持@include递归引入;同时加载.claude/rules/*.md。 - 解决:把"规则"从一次性文本变成可维护模块,团队可演进。
- 怎么做 :
-
【设计 4:动态环境事实注入(cwd/git/status)】
- 怎么做:把工作目录、平台、shell、git branch/log/status 等拼到 dynamic context。
- 解决:减少"模型猜环境",让它做出更贴近真实仓库状态的决策。
-
【设计 5:近因效应的排序策略(把高价值动态内容放后面)】
- 怎么做:记忆/技能/agent 描述等放在动态块末尾,让模型更容易"近期看到"。
- 解决:提高"该用技能/该召回记忆/该用子 agent"的触发率。
-
【设计 6:与其他子系统对齐(先读后改/危险操作确认/并发规则)】
- 怎么做:在静态核心里写清"read-before-edit""危险操作需确认""并行只读/串行写"等产品规则,与工具/权限/并发实现一致。
- 解决:减少策略漂移:提示词与工具真实行为一致,模型更可控。
4.【实例】通过一个实例多轮跑来完整覆盖展示每个设计在实例中具体是如何工作的
仍然用同一个任务:
"修复单元测试失败:
test_total_should_round。修好后跑全量测试。然后做一次小重构(提取函数/清理命名),最后给我简短总结。"
Turn 0:会话刚开始就把"行为宪法"装进请求(静态 vs 动态 vs reminder)
在你输入第一句话之前,系统已经把一次模型调用会带的三块内容准备好:
- system 静态核心:例如"先读后改、危险操作确认、如何用工具"。
【设计 1:静态核心】 - system 动态环境:例如
Working directory、Platform/Shell、Git status。
【设计 1/4:动态环境事实】 <system-reminder>:例如CLAUDE.md+ 今天日期。
【设计 2:system-reminder 注入】
这一步的"体感结果"是:你在不同仓库输入同一句话,它会遵守不同项目规则,但基本行为(先读、可并行读、危险确认)很稳定。
Turn 1:为什么它总是先读再改(不是"它懂",是规则强约束)
你说"修复测试失败",模型第一轮通常会先读/搜:
tool_use:
grep_search(...)tool_use:
read_file(...)
这不是随机的"好习惯",而是静态核心里写了"Do not propose changes to code you haven't read. Read files first." 这种规则。
【设计 6:与工具行为对齐(先读后改)】
Turn 2:为什么它能"知道在什么分支/有啥改动"(动态环境事实在起作用)
当它要决定"是否建议你 commit / 是否建议先 stash / 是否该重跑全量测试"时,动态块里的 git context 会影响它的策略:
(模型在文本里会提到)"当前分支是 ...,工作区有未提交改动 ..."
【设计 4:动态环境事实】不是靠它猜,而是系统把 git 信息塞进了 dynamic context。
Turn 3:项目规则如何"精确改写它的行为"(CLAUDE.md + include)
假设你的 CLAUDE.md 里写了:
"不要改格式化配置;所有改动必须跑
pytest -q;禁止git push。"
那么你会看到它在策略上变化:
- 仍然能改 bug,但会更主动跑测试
- 避免触碰被禁止的文件
- 遇到 push 会停下来说明原因
【设计 2:system-reminder 注入】项目规则进入提醒,不污染静态核心。
【设计 3:@include/规则组合】大团队可以把这些规则拆成模块复用。
Turn 4:为什么"技能/记忆/子 agent"更容易被用上(排序与近因效应)
当 dynamic context 的末尾出现:
- 技能列表(例如
/commit) - 记忆片段(例如"本仓库用 ruff")
- 子 agent 描述(例如"test-runner agent")
你会在同一任务后半程更容易看到它说:
"我建议调用
/commit生成提交说明"或 "我记得这个仓库要求用 ruff/black"
或 "我可以派一个子 agent 跑全量测试并汇总"
【设计 5:排序策略】把"想让它常用的东西"放在更靠后、更显著的位置。
5.【System Prompt】总结
- System Prompt 是"行为宪法":它把产品策略固化成每轮都稳定生效的规则与偏好。
- 你应带走的记忆点:
- 静态/动态分层 + reminder 注入(设计 1/2)
- 规则模块化(@include / rules)(设计 3)
- 动态环境事实让模型少猜、多基于事实决策(设计 4)
- 排序影响触发率(近因效应)(设计 5)
- 提示词必须与工具/权限真实行为对齐(设计 6)
第七部分:CLI 与会话(REPL / 中断 / 持久化 / Resume)
1.【CLI 与会话】在产品链里的位置
CLI 与会话层位于用户与 Agent Loop 之间,是"把内核产品化"的外壳:
- 上游:用户在终端输入一条 prompt 或进入 REPL 逐句对话
- 中游:CLI 解析参数(
--resume、--yolo、--plan、--max-turns...),把"运行策略"配置进 Agent - 下游:每轮执行后把对话历史写到磁盘;下次可
--resume恢复继续;Ctrl+C 能打断当前一轮
一句话:Agent Loop 负责"做事",CLI/Session 负责"可用、可控、可恢复"。
2.【CLI 与会话】要解决产品的什么问题
- 让系统从"库"变成"工具":没有 CLI/REPL,用户很难把它当成日常工作流的一部分。
- 让长任务可恢复:进程退出/网络中断后,必须能恢复继续,而不是从零再来。
- 让用户能随时打断:真实协作里用户会发现方向不对或想插一句,Ctrl+C/命令必须能介入。
- 让"运行策略"可配置:同一个 agent 在本地交互、在 CI、在团队仓库里风险策略/预算/模型都不同。
3.【CLI 与会话】是通过几类什么设计,分别如何解决的问题
我们把本部分归纳成 6 类设计点(后面的实例会全部跑出来):
-
【设计 1:入口模式(One-shot vs REPL)】
- 怎么做:有 prompt → 单次模式执行并退出;无 prompt → 进入 REPL 循环逐句 chat。
- 解决:既能脚本化,也能交互式协作。
-
【设计 2:参数解析 → 运行策略注入】
- 怎么做:CLI 把 flags 解析成 Agent 配置:权限模式、model/base_url、thinking、max_cost/max_turns 等。
- 解决:把"产品策略"从代码硬编码变成可组合开关。
-
【设计 3:会话持久化(messages 落盘)】
- 怎么做:每轮把 messages 数组保存为 JSON;这是最小的可恢复状态。
- 解决:进程退出不丢上下文,长任务可续跑。
-
【设计 4:--resume 恢复(把历史重新装回 Agent)】
- 怎么做 :启动时读取 session 文件,调用
agent.loadHistory()恢复。 - 解决:把"会话"从一次性对话变成可持续的工作线程。
- 怎么做 :启动时读取 session 文件,调用
-
【设计 5:REPL 命令(/clear /cost /compact /plan ...)】
- 怎么做:用命令直接操控会话与运行态:清空历史、触发压缩、切换模式等。
- 解决:把"调试与治理能力"交给用户,而不是只能重启进程。
-
【设计 6:中断与收敛(Ctrl+C / abort / turn 限制)】
- 怎么做 :用户中断当前一轮;或通过
--max-turns/--max-cost让会话在可控边界停止。 - 解决:防止跑飞,保证人类始终能夺回控制权。
- 怎么做 :用户中断当前一轮;或通过
4.【实例】通过一个实例多轮跑来完整覆盖展示每个设计在实例中具体是如何工作的
仍然用同一个任务:
"修复单元测试失败:
test_total_should_round。修好后跑全量测试。然后做一次小重构(提取函数/清理命名),最后给我简短总结。"
下面这段"运行故事"重点不讲代码细节,只讲你在终端里会看到什么、系统内部发生了什么。
Turn 0:你选择运行模式(One-shot vs REPL)
你有两种常见启动方式:
- One-shot(单次):
mini-claude "修复单元测试失败..."→ 跑完这一条就退出
【设计 1:入口模式】
- REPL(交互):
mini-claude→ 进入提示符,逐句对话
【设计 1:入口模式】
两种模式的区别不是"agent 能力不同",而是"用户交互方式不同"。
Turn 1:CLI flags 把运行策略注入到这一段会话里
你带着策略启动:
--plan:只读分析(写入与 shell 受限)--yolo:更激进(但仍受 deny 规则约束)--max-turns 20:最多 20 轮,防止跑飞
你在实例里会感受到:同一句"修复测试失败",在 --plan 下它会先输出方案而不是直接 edit;在 --max-turns 下到上限会自动停下并汇报进度。
【设计 2:参数解析→策略注入】
【设计 6:turn 限制】
Turn 2:每轮之后自动保存会话(messages 落盘)
你在 REPL 里输入第一句后,Agent Loop 跑完一轮(可能包含多次 tool_use),然后 CLI 立刻保存会话:
(你看不到文件写入,但你会看到下次还能恢复)
【设计 3:会话持久化】核心是:保存的是 messages,因为它就是 Agent 的最小状态。
Turn 3:进程退出后用 --resume 继续(恢复长任务)
你关掉终端或进程崩了,第二天回来:
mini-claude --resume "继续刚才的修复:先跑全量测试,然后总结"
它会提示:
"(resumed N messages) ..."
然后继续在同一上下文里推进,而不是从零开始。
【设计 4:--resume 恢复】
Turn 4:你用 REPL 命令直接治理会话(/clear /compact)
当你发现"历史太乱/模型开始重复读文件",你可以:
/compact:手动触发压缩(把桌面收拾一下)
【设计 5:REPL 命令】/clear:清空历史,重新开始(同时更新 session)
【设计 5:REPL 命令】
你会在行为上看到:/compact 之后模型更常按需 re-read;/clear 之后它像"新会话"一样重新建立事实。
Turn 5:Ctrl+C 中断当前一轮(人类夺回控制权)
当模型正在流式输出、或正在等待某个慢命令时,你按 Ctrl+C:
- 当前一轮会被中断(网络请求/工具执行被 abort)
- REPL 不退出,你可以立刻输入新指令纠偏:
- "停一下,别跑全量测试,先只跑这个文件的测试"
【设计 6:中断与收敛】这是"人类在环"的关键体验:你不需要等它跑完才能纠正方向。
5.【CLI 与会话】总结
- CLI/Session 的价值是把 agent 变成"可持续使用的产品":能交互、能恢复、能打断、能配置策略。
- 你应带走的记忆点:
- 两种入口形态(one-shot vs REPL)(设计 1)
- flags 决定运行策略(设计 2)
- messages 落盘是最小可恢复状态(设计 3/4)
- REPL 命令提供治理面板(设计 5)
- Ctrl+C/turn 限制保证人类控制权(设计 6)
第八部分:记忆系统(Memory:跨会话长期记忆)
1.【记忆系统】在产品链里的位置
记忆系统位于 Agent Loop 的"每次调用模型之前",它把跨会话的长期信息按需注入到 system prompt / user messages 中:
- 上游:用户在不同天、不同会话反复提到偏好/项目事实(例如"部署去 staging""不要在末尾总结")
- 中游:记忆以小文件形式落盘;每次新请求时根据"当前问题"召回相关记忆
- 下游:被召回的记忆成为模型本轮决策的前提,影响工具选择、风险偏好与输出格式
一句话:上下文工程解决"本会话别爆窗",记忆系统解决"跨会话别全忘"。
2.【记忆系统】要解决产品的什么问题
- 跨会话遗忘:如果只有 messages 历史,会话一关就"失忆",下一次要从零重新解释。
- 不能把所有历史都背回来:哪怕你把旧会话全存起来,真正调用模型时也不可能把所有历史塞进窗口(成本/长度/噪声都不允许)。
- 相关性问题:记忆应该"按当前任务相关"被召回,而不是每次都把所有偏好/事实灌进去。
- 可维护性:记忆要能被人类审阅、编辑、删除(不能是不可读的向量库黑盒)。
3.【记忆系统】是通过几类什么设计,分别如何解决的问题
我们把记忆系统归纳成 6 类设计点(后面的实例会全部跑出来):
-
【设计 1:记忆落盘(每条记忆一个文件)】
- 怎么做 :把偏好/事实写成
.md小文件,并维护一个MEMORY.md索引。 - 解决:跨会话持久化 + 人可读可审阅。
- 怎么做 :把偏好/事实写成
-
【设计 2:项目级命名空间(按 cwd 哈希分隔记忆空间)】
- 怎么做:同一项目目录映射到同一记忆目录,不同项目互不污染。
- 解决:避免把 A 项目的事实带到 B 项目造成"错记"。
-
【设计 3:召回策略(按相关度取 Top-K,而不是全量注入)】
- 怎么做:对当前 query 计算相关度(最小实现可用词重叠;生产版可用语义选择/sideQuery)。
- 解决:把有限的上下文预算留给"最相关的几条"。
-
【设计 4:注入位置(system prompt vs user message)】
- 怎么做:部分记忆进入 system prompt 的 Memory section;也可以把"召回结果"作为一条 user message 注入。
- 解决:不同性质的记忆有不同权重与作用方式(偏好/约束更适合放系统侧)。
-
【设计 5:异步预取/sideQuery(提前捞记忆,减少等待)】
- 怎么做:在主模型输出前并行启动记忆召回(生产版常见:用一个 sideQuery 让模型挑相关记忆)。
- 解决:降低"召回造成的额外延迟",并提升相关性质量。
-
【设计 6:与压缩/Resume 的协作】
- 怎么做 :即使会话被压缩成摘要、或你
--resume只恢复了最近历史,记忆仍能在关键节点补回长期偏好与项目事实。 - 解决:长任务不"断片",长期偏好更稳定生效。
- 怎么做 :即使会话被压缩成摘要、或你
4.【实例】通过一个实例多轮跑来完整覆盖展示每个设计在实例中具体是如何工作的
仍然用同一个任务(并加入一个"跨会话复用"的场景):
Day1:你说"修复测试失败...最后给我简短总结"。
Day2:你
--resume或新开会话继续类似任务,并问"应该部署到哪里验证?"。
Turn 0(Day1):会话内产生"可长期复用的信息"
你在过程中明确提出偏好/事实:
- "部署请走 staging"
- "输出尽量简短,不要最后再写一段总结"
系统把它们写成记忆文件(每条一个 .md),并更新 MEMORY.md 索引。
【设计 1:记忆落盘】
【设计 2:项目命名空间】(这些记忆只属于当前仓库)
Turn 1(Day2):你新开会话,但它依然"记得"
你第二天问:
"Where should I deploy my changes to test them?"
系统在调用模型前执行召回:从该项目记忆目录里挑出与 "deploy / staging / test" 最相关的 Top-K。
【设计 3:召回 Top-K】
被召回的记忆被拼进 system prompt 的 Memory 段(或作为一条 user message 注入),于是模型直接回答:
"Deploy to staging ..."
【设计 4:注入位置】你看到的是"它直接知道",而不是先问你一遍再决定。
Turn 2:压缩/摘要之后它仍能保持长期偏好
当会话很长触发了上下文压缩(第三部分),早期的"偏好说明"可能不再在 messages 里逐字可见。
但下一轮它仍会保持"输出简短/避免冗余总结",因为这条偏好是从记忆系统召回进入 prompt 的。
【设计 6:与压缩协作】
Turn 3:为什么它不会把所有记忆都塞进来(相关性与预算)
你问一个完全不相关的问题(例如"这个测试为什么浮点误差"),系统不会把"部署到 staging"这种记忆塞进 prompt,因为相关度低。
【设计 3:只召回相关 Top-K】这能显著减少噪声与 token 浪费。
Turn 4:预取/sideQuery 让它"既相关又不慢"
在更生产级的实现里,系统会在你输入后立刻并行启动记忆预取(甚至用 sideQuery 让模型挑最相关记忆),把这段时间藏在 streaming 的窗口里。
【设计 5:异步预取/sideQuery】
5.【记忆系统】总结
- 记忆系统的核心是:把跨会话的偏好/事实从"历史里找"变成"按需召回注入"。
- 你应带走的记忆点:
- 文件化记忆 + 索引(设计 1)
- 项目级隔离避免污染(设计 2)
- Top-K 相关召回控制噪声与成本(设计 3)
- 注入位置影响权重与行为(设计 4)
- 预取提升体验(设计 5)
- 与压缩/Resume 协同保证长期一致性(设计 6)
第九部分:技能系统(Skills:把高频提示词变成"可调用模块")
1.【技能系统】在产品链里的位置
技能系统位于两条入口路径上:
- 用户入口(显式调用) :你在 CLI/REPL 里输入
/commit→ CLI 解析为技能 prompt → 进入同一个 Agent Loop。 - 模型入口(隐式调用):模型在任务进行中判断"该用某个技能",通过 tool 调用触发技能(或 fork 子 agent 执行)。
同时,技能定义会被注入到 System Prompt(作为可用能力清单),让模型"知道有这些可复用的模块"。
一句话:工具是"原子动作",技能是"复合动作模板"。
2.【技能系统】要解决产品的什么问题
- 高频工作流重复:例如"看 diff → 写 commit message → 提交",每次临时写提示词成本高、质量不稳定。
- 把团队 SOP 产品化:团队希望把规范写成可复用技能,而不是靠口头约定或每次手动提示。
- 安全边界 :技能往往会调用工具(例如
run_shell),需要能限制"技能允许用哪些工具"。 - 可扩展:新增/修改技能不应改源码;像脚本一样"即装即用"。
3.【技能系统】是通过几类什么设计,分别如何解决的问题
我们把技能系统归纳成 6 类设计点(后面的实例会全部跑出来):
-
【设计 1:技能即文件(SKILL.md / name.md)】
- 怎么做:一个技能就是一个文件:frontmatter(元信息)+ prompt 模板正文。
- 解决:人可读、可版本管理、可共享。
-
【设计 2:发现与加载(user vs project,项目覆盖用户)】
- 怎么做:扫描用户目录与项目目录的技能;同名时项目级覆盖用户级。
- 解决:个人习惯与项目规范可叠加,且项目规则优先。
-
【设计 3:解析与模板变量(frontmatter + $ARGUMENTS 等)】
- 怎么做 :解析 frontmatter 字段(
when_to_use/allowed-tools/user-invocable),并支持把用户参数注入模板。 - 解决:让技能从"死文本"变成可参数化模块。
- 怎么做 :解析 frontmatter 字段(
-
【设计 4:注入 System Prompt(让模型知道"有哪些技能可用")】
- 怎么做:把技能描述(名字/用途/触发条件)拼进 dynamic system context。
- 解决 :模型能在合适时机主动选择技能,而不是永远靠用户记住
/name。
-
【设计 5:两种执行方式(inline vs fork)】
- 怎么做:inline 把技能 prompt 直接拼进当前会话;fork 把技能交给干净的子 agent 单独跑再返回摘要。
- 解决:既能低成本复用,也能隔离复杂步骤/减少主会话污染。
-
【设计 6:技能的安全边界(allowed-tools)】
- 怎么做:技能声明允许使用的工具集合,超出则拒绝或降级。
- 解决:把"技能能做什么"收敛进可治理边界,避免技能成为越权通道。
4.【实例】通过一个实例多轮跑来完整覆盖展示每个设计在实例中具体是如何工作的
仍然用同一个任务:
"修复单元测试失败:
test_total_should_round。修好后跑全量测试。然后做一次小重构(提取函数/清理命名),最后给我简短总结。"
我们重点看两类"技能触发时刻":用户显式触发 与模型隐式触发。
Turn 0:会话启动时,技能被发现并注入到 system(模型知道"能用什么")
你启动会话时,系统扫描:
~/.claude/skills/*(用户技能).claude/skills/*(项目技能,覆盖用户)
并把技能的"名称/描述/when_to_use"注入到 system dynamic context。
【设计 2:发现与覆盖】
【设计 4:注入 System Prompt】
你在行为上会感受到:模型会更自然地说"我可以用 /commit 技能来生成提交信息",而不是你必须先教它。
Turn 1:用户显式调用技能(/commit)
当你修完 bug、测试通过,你在 REPL 输入:
/commit
CLI 把 /commit 解析为对应技能文件的 prompt 模板,并(可选)把你的参数附在 $ARGUMENTS。
【设计 1:技能即文件】
【设计 3:模板参数】
最终进入 Agent Loop 的其实是一段"展开后的技能 prompt",模型据此生成 commit message(或指导下一步)。
【设计 5:inline 执行】(最简单路径:直接拼进当前会话)
Turn 2:模型隐式触发技能(when_to_use → tool 调用 skill)
你没有输入 /commit,只说:
"修好后帮我提交。"
因为 system 里注入了技能元信息,模型会判断命中 when_to_use,主动触发技能:
【设计 4:注入 System Prompt】模型"看见了技能清单",才有可能自动触发。
Turn 3:技能的工具边界生效(allowed-tools)
假设 /commit 技能声明:
allowed-tools: run_shell, read_file
技能执行过程中如果它想调用一个不在白名单的工具(例如 edit_file),系统会拒绝/要求改写策略:
【设计 6:allowed-tools 边界】
你在实例里会看到它把策略降级为:
"我不能在该技能里编辑文件,但我可以给出建议/请你在主会话里执行 edit。"
Turn 4:什么时候需要 fork(把复杂技能隔离出去)
如果技能本身是多步且会产生大量中间输出(例如"跑全量测试并总结失败"),更适合用 fork:
- 主会话:只看到"我启动了一个子 agent 去做 X"
- 子会话:自己读文件/跑命令/汇总
- 返回:给主会话一个短摘要(避免污染主上下文)
【设计 5:fork 执行】这本质是"把复合工作流隔离成一个可回收的子线程"。
5.【技能系统】总结
- 技能系统把"高频提示词/工作流"模块化:让它既能被用户像命令一样调用,也能被模型在合适时机自动触发。
- 你应带走的记忆点:
- 技能即文件 + frontmatter 元信息(设计 1/3)
- 用户/项目两级发现与覆盖(设计 2)
- 注入 system 让模型可自动选择(设计 4)
- inline vs fork 两种执行形态(设计 5)
- allowed-tools 把技能纳入治理边界(设计 6)
第十部分:Plan Mode(先计划、后执行的只读审批工作流)
1.【Plan Mode】在产品链里的位置
Plan Mode 位于"用户目标 → 实际写入执行"之间,插入一个只读规划阶段:
- 上游:用户给出复杂/高风险任务(重构、迁移、批量改动)
- 中游:进入 Plan Mode 后,Agent 只能读/搜/分析,把方案写进 plan 文件并退出
- 下游:用户审批后,才切回可执行模式(例如 acceptEdits)开始真正改代码/跑命令
一句话:Plan Mode 把"先对齐预期再动手"产品化。
2.【Plan Mode】要解决产品的什么问题
- 控制风险:不希望 agent 一上来就改一堆文件;先看计划、范围、风险点。
- 减少返工:先把"要改什么/怎么验证/影响范围"写清楚,避免执行到一半才发现方向错。
- 让只读承诺可被强制:Plan Mode 的"只读"不能靠提示词劝告,而必须由权限系统在代码层面强制。
- 把审批流程标准化:给用户明确的 4 种选择(照做/改改再做/手动执行/继续规划)。
3.【Plan Mode】是通过几类什么设计,分别如何解决的问题
我们把 Plan Mode 归纳成 6 类设计点(后面的实例会全部跑出来):
-
【设计 1:只读强制(plan 权限模式)】
- 怎么做 :进入 plan 后禁止
edit_file/write_file/run_shell(在权限闸门上叠加约束)。 - 解决:把"只读"变成不可突破的产品契约。
- 怎么做 :进入 plan 后禁止
-
【设计 2:显式进入/退出工具(enter_plan_mode / exit_plan_mode)】
- 怎么做:提供两个状态切换工具,让模型能在同一个 Agent Loop 里进入规划与退出交付。
- 解决:Plan Mode 不是另一个程序,而是同一循环里的一个阶段。
-
【设计 3:计划落盘(plan 文件)】
- 怎么做:把计划写到一个 plan 文件(会话独立路径),供用户审阅。
- 解决:计划成为可审阅工件,而不是一段易丢的聊天文本。
-
【设计 4:审批工作流(4 选项)】
- 怎么做 :退出 plan 后进入审批:
- Clear + Execute(清空上下文再执行)
- Execute(保留上下文直接执行)
- Manual(回到原模式,用户手动执行)
- Keep Planning(继续规划并反馈)
- 解决:把"人类在环"做成明确状态机。
- 怎么做 :退出 plan 后进入审批:
-
【设计 5:模式可逆(prePlanMode 精确恢复)】
- 怎么做:记录进入前的权限模式(default/acceptEdits/...),退出时恢复,而不是一刀切回 default。
- 解决:Plan Mode 不破坏用户原本的运行策略。
-
【设计 6:延迟加载(deferred plan tools)】
- 怎么做:enter/exit 工具标记 deferred,只有需要 plan 时才注入 tools schema。
- 解决:不污染大多数普通会话的上下文预算(与上下文工程一致)。
4.【实例】通过一个实例多轮跑来完整覆盖展示每个设计在实例中具体是如何工作的
仍然用同一个任务:
"修复单元测试失败:
test_total_should_round。修好后跑全量测试。然后做一次小重构(提取函数/清理命名),最后给我简短总结。"
这次我们刻意把任务变成"高风险/多文件改动",触发 Plan Mode 的典型用法。
Turn 0:你要求先 plan(进入只读阶段)
你输入:
"先别改代码,先给我一个 plan。批准后再执行。"
模型触发:
tool_use:
enter_plan_mode({})
系统切换到 plan 模式,并在权限层强制只读。
【设计 1:只读强制】
【设计 2:进入工具】
Turn 1:在 plan 模式中它只能读/搜(写入与 shell 会被拦)
模型试图"写计划到文件":
tool_use:
write_file({ file_path: "report.txt", content: "..." })
tool_result 返回拒绝:
"Denied: write_file was blocked (plan mode)."
【设计 1:只读强制】你在实例里能直接看到"它想写,但系统硬拦",不是靠自觉。
于是模型改为只读探索:
tool_use:
grep_search(...)tool_use:
read_file(...)
Turn 2:计划作为工件落盘(写入只允许写 plan 文件)
在更完整的 Plan Mode 里,系统会生成一个 plan 文件路径(会话专属),并允许"只写这一个文件"。模型把计划写进去:
tool_use:
write_file({ file_path: "<plan_file_path>", content: "<plan 内容>" })
【设计 3:计划落盘】计划成为可审阅文件而不是聊天片段。
Turn 3:退出 plan → 进入审批(4 选项)
模型完成计划后:
tool_use:
exit_plan_mode({})
系统弹出审批选择(概念上):
-
- Clear + Execute
-
- Execute
-
- Manual
-
- Keep Planning
【设计 4:审批工作流】这是把协作做成"明确状态机"的关键。
Turn 4:你选择 Execute(保留上下文,切到 acceptEdits 执行)
你选择:
"2) Execute"
系统把权限模式切到可执行(例如 acceptEdits),并开始按 plan 执行:
【设计 4:审批→执行】
【设计 5:模式可逆】(退出后恢复到应有策略,而不是乱跳)
接下来同一个 Agent Loop 才会真的进入:
tool_use:
edit_file(...)tool_use:
run_shell({ command: "pytest -q" })
Turn 5:你选择 Keep Planning(反馈→继续只读规划)
如果你觉得 plan 不满意,选择:
"4) Keep Planning:把风险点写清楚,并补一个验证 checklist"
系统仍保持 plan 模式,只读继续迭代计划:
【设计 4:Keep Planning 回路】
5.【Plan Mode】总结
- Plan Mode 的核心是"先对齐再执行",并且 只读承诺必须由权限系统强制。
- 你应带走的记忆点:
- plan 不是提示词,是权限层强制只读(设计 1)
- enter/exit 让它成为同一循环里的阶段(设计 2)
- 计划落盘成为可审阅工件(设计 3)
- 审批 4 选项把协作做成状态机(设计 4)
- 退出要能恢复原模式(设计 5)
- deferred 工具避免污染多数会话(设计 6)
第十一部分:多 Agent(Multi-Agent:Sub-Agent fork-return)
1.【多 Agent】在产品链里的位置
多 Agent 发生在主 Agent Loop 内部:主 agent 在某一轮决策中,通过一个 agent(...) 工具把子任务"外包"给子 agent,然后把子 agent 的结果作为 tool_result 回注,继续主循环。
- 上游:主 agent 发现任务太大/探索面太广/需要并行
- 中游:
tool_use: agent({ task, type? })→ 系统启动子 agent(独立上下文、通常只读/受限工具) - 下游:子 agent 返回摘要(不是全部过程)→ 主 agent 基于摘要继续执行主任务
一句话:主 agent 做协调与收敛,子 agent 做探索与啃硬骨头。
2.【多 Agent】要解决产品的什么问题
- 上下文容量瓶颈:大任务全塞进一个会话,messages 很快被工具结果/探索过程撑爆。
- 并行探索需求:很多任务需要同时查多个点(测试失败根因、依赖版本、配置文件、相关模块)。
- 降噪:探索阶段产生大量中间日志/路径/片段,不应全部污染主会话。
- 风险控制:子 agent 往往只需要读工具;限制它的工具集能显著降低副作用风险。
3.【多 Agent】是通过几类什么设计,分别如何解决的问题
我们把多 Agent 归纳成 6 类设计点(后面的实例会全部跑出来):
-
【设计 1:agent 工具(主循环中的子任务外包接口)】
- 怎么做 :主 agent 通过
agent({ task })请求系统运行子 agent,并把结果作为tool_result回注。 - 解决:把"并行/外包"变成主循环中的标准工具调用。
- 怎么做 :主 agent 通过
-
【设计 2:上下文隔离(子 agent 独立 messages)】
- 怎么做:子 agent 使用独立消息数组,从干净上下文开始啃任务。
- 解决:避免主会话被探索过程撑爆,降低互相干扰。
-
【设计 3:工具白名单(只读子 agent)】
- 怎么做 :Explore/Plan 子 agent 只给
read_file/list_files/grep_search;从工具层面 deny 写入与 shell。 - 解决:比"提示词说别乱改"更稳的安全边界。
- 怎么做 :Explore/Plan 子 agent 只给
-
【设计 4:子 agent 类型化(explore / plan / general)】
- 怎么做:不同子 agent 用不同 system prompt 与工具集:explore 快速只读探索;plan 产出结构化方案;general 处理独立可执行任务。
- 解决:让外包更可控、更贴合任务形态。
-
【设计 5:返回的是摘要(fork-return,而非全量回放)】
- 怎么做:子 agent 最终只返回一段简明总结/关键路径/文件列表。
- 解决:主会话"拿到结论"而不是"背回全部过程",显著降噪。
-
【设计 6:与其他系统协作(权限/上下文/流式)】
- 怎么做:子 agent 同样遵守权限/并发规则;其输出作为普通 tool_result 进入上下文工程与压缩流水线。
- 解决:多 agent 不是特例,而是"工具系统的一个扩展点"。
4.【实例】通过一个实例多轮跑来完整覆盖展示每个设计在实例中具体是如何工作的
仍然用同一个任务:
"修复单元测试失败:
test_total_should_round。修好后跑全量测试。然后做一次小重构(提取函数/清理命名),最后给我简短总结。"
这次我们把"探索阶段"外包给子 agent,让你看到 fork-return 在运行时长什么样。
Turn 1:主 agent 发现需要探索(但不想污染主上下文)
主 agent 在第一轮会想做很多只读探索(找失败测试、定位实现、找相关配置)。如果全在主会话做,会产生大量 grep/read 结果。
于是它选择外包:
tool_use:
agent({ task: "定位 test_total_should_round 的测试位置与实现位置,并列出相关文件路径。" })
【设计 1:agent 工具】外包是一个 tool_use。
【设计 4:子 agent 类型化】这通常落到 explore 子 agent。
Turn 2:子 agent 在独立上下文里只读探索(工具白名单生效)
子 agent 自己执行:
tool_use:
grep_search(...)tool_use:
read_file(...)
它即使"想跑 pytest/想 edit",也会被工具白名单拒绝:
【设计 3:只读子 agent】从工具层面锁死副作用。
Turn 3:子 agent 返回"摘要"而不是全部过程
子 agent 最终返回(作为 tool_result):
tool_result: "测试在 tests/test_total.py:42;实现相关在 src/calc.py:10;可能关联配置在 pyproject.toml..."
【设计 5:返回摘要】你会发现:主会话只得到"结论+路径",没有被几十屏 read_file 内容污染。
【设计 2:上下文隔离】探索过程留在子上下文里。
Turn 4:主 agent 基于摘要继续执行主线(读→改→跑)
主 agent 现在更聚焦:
tool_use:
read_file({ file_path: "src/calc.py" })tool_use:
edit_file(...)tool_use:
run_shell({ command: "pytest -q" })
【设计 6:与权限/上下文/流式协作】主会话仍走原本的工具治理与压缩策略,多 agent 只是帮它减少探索噪声。
Turn 5:需要计划时再派 Plan 子 agent(结构化计划回传)
当进入"重构(提取函数/清理命名)"阶段,主 agent 可能再外包一个 Plan 子 agent:
tool_use:
agent({ task: "给出重构计划:要改哪些文件、每步如何验证、潜在风险。" , type: "plan" })
子 agent 返回结构化计划摘要,主 agent 再进入 Plan Mode 或直接执行。
【设计 4:类型化(plan)】
【设计 5:返回摘要】(计划是摘要化的工件)
5.【多 Agent】总结
- 多 Agent 的本质是"分而治之":把探索/规划外包到独立上下文里,主会话只拿回关键结论,既省上下文又更可控。
- 你应带走的记忆点:
- agent 工具把外包变成标准 tool_use(设计 1)
- 独立上下文避免污染主会话(设计 2)
- 只读白名单把风险锁死(设计 3)
- explore/plan/general 类型化(设计 4)
- fork-return 返回摘要而非全程(设计 5)
- 与权限/上下文/流式一致协作(设计 6)
第十二部分:MCP 集成(Model Context Protocol:外部工具动态挂载)
1.【MCP】在产品链里的位置
MCP 位于工具系统的"工具来源层":它让工具不再只能写死在代码里,而是可以从外部服务器动态发现并注册。
- 上游:用户/项目配置声明 MCP 服务器(例如 filesystem / github / database)
- 中游:Agent 启动时连接 MCP(spawn 子进程 + stdio JSON-RPC),拉取
tools/list动态发现工具 - 下游:模型发起
tool_use: mcp__<server>__<tool>→ 框架把调用路由回 MCP server 执行 → 结果以tool_result回注
一句话:内置工具是"内置插件",MCP 是"外置插件系统"。
2.【MCP】要解决产品的什么问题
- 扩展性:不改 agent 源码就能接入新能力(DB、Slack、GitHub、内部 API、K8s...)。
- 统一治理:外部工具也要走同一套 schema 校验、权限、并发、审计与错误回注。
- 解耦:agent 不需要知道"怎么连 GitHub",只需要知道"有一个工具能查 PR、能读 CI 状态"。
- 按项目配置 :不同仓库需要不同工具集(项目级
.mcp.json/.claude/settings.json)。
3.【MCP】是通过几类什么设计,分别如何解决的问题
我们把 MCP 集成归纳成 6 类设计点(后面的实例会全部跑出来):
-
【设计 1:配置驱动的服务器声明(mcpServers 合并覆盖)】
- 怎么做:用户级/项目级/settings/.mcp.json 声明服务器 command/args/env;同名后读覆盖先读。
- 解决:工具扩展从"改代码"变成"改配置"。
-
【设计 2:连接管理器(McpManager / McpConnection)】
- 怎么做:负责 spawn 进程、握手 initialize、维护连接生命周期、缓存已发现工具。
- 解决:把"协议细节"封装成一层,主循环只关心"有哪些工具、怎么调用"。
-
【设计 3:JSON-RPC over stdio(最小可用传输)】
- 怎么做:通过 stdin/stdout 按行 JSON 发送请求与接收响应(initialize / tools/list / tools/call)。
- 解决:最小依赖、跨语言、易调试(教学实现尤其适合)。
-
【设计 4:工具发现与前缀注册(mcp__server__tool)】
- 怎么做:把服务器返回的工具注册到 tools 列表里,并加前缀防止命名冲突。
- 解决:对模型来说外部工具"长得像内置工具",调用方式一致。
-
【设计 5:透明路由(tool_use → callTool)】
- 怎么做 :遇到
mcp__...工具调用时不走本地 executor,而是路由到对应 server 的tools/call。 - 解决:主循环不需要知道外部工具实现,只要转发与回注即可。
- 怎么做 :遇到
-
【设计 6:外部工具也要治理(权限/并发/大结果)】
- 怎么做:MCP 工具输出同样进入 tool_result,并被上下文工程裁剪/落盘;危险调用仍需权限 gate。
- 解决:扩展不会破坏安全与稳定性边界。
4.【实例】通过一个实例多轮跑来完整覆盖展示每个设计在实例中具体是如何工作的
仍然用同一个任务(加一点"外部能力依赖"让 MCP 的价值出现):
"修复单元测试失败:
test_total_should_round。修好后跑全量测试。然后做一次小重构,最后给我简短总结。另外:如果 CI 里也失败了,帮我把失败的 job 日志摘要出来。"
Turn 0:启动时连接 MCP 并发现工具(但 agent 源码不变)
你在项目配置里声明了一个 MCP server(例如 github 或 ci)。启动会话后,系统:
- spawn server 子进程(stdio)
initialize握手tools/list拉取外部工具清单- 把工具注册进 tools 列表(带前缀)
【设计 1:配置驱动】
【设计 2/3:连接管理 + JSON-RPC】
【设计 4:前缀注册】
Turn 1:主任务照常推进(MCP 不干扰内置工具)
模型依然会先读/搜/跑测试:
tool_use:
grep_search(...)tool_use:
read_file(...)tool_use:
run_shell({ command: "pytest -q" })
MCP 工具并不会"抢流程",它只是多了一组可用能力。
【设计 4:外部工具像内置工具一样存在于 tools 清单里】
Turn 2:当需要外部事实时,模型调用 mcp__ 工具(透明路由)
当模型需要 CI 失败日志,它会调用类似:
tool_use:
mcp__github__get_ci_status({ ... })tool_result: "CI failed: job=test ... run_id=..."
或:
tool_use:
mcp__ci__fetch_job_log({ run_id: "...", job: "test" })
系统看到 mcp__ 前缀,直接路由到 MCP server 的 tools/call,而不是本地 executor:
【设计 5:透明路由】
Turn 3:外部工具的结果同样进入上下文治理(大结果落盘/裁剪)
CI 日志可能非常长:
tool_result: "Output too large... saved to ... + preview"
模型再按需读关键段落或让 MCP server 直接返回摘要:
【设计 6:外部工具也要治理】(和第三部分上下文工程一致)
Turn 4:失败也是数据(外部调用失败 → tool_result → 改策略)
如果 MCP server 断开/鉴权失败:
tool_result: "Error: unauthorized / server not reachable ..."
模型会改策略:让你配置 token、或退回本地 run_shell/手动指引,而不是会话崩溃。
【设计 6:错误回注可恢复】
5.【MCP】总结
- MCP 的核心价值是"插件化外部工具":新增能力不改 agent 代码,只改配置与 server。
- 你应带走的记忆点:
- 配置声明 server → 动态发现 tools(设计 1/4)
- JSON-RPC over stdio 最小实现(设计 2/3)
- mcp__ 前缀 + 透明路由(设计 4/5)
- 外部工具仍然必须走治理与上下文管控(设计 6)
第十三部分:功能测试与验收(Testing:把"模块"跑成"产品")
1.【功能测试】在产品链里的位置
功能测试位于"所有模块实现完成之后",它不是某一个子系统,而是用一组端到端场景去验证:
- 单个模块是否真的可用(工具/权限/记忆/技能/子 agent/MCP...)
- 模块组合后是否仍然成立(例如:流式 + 并发 + 权限 + 大结果落盘 + 压缩 + resume)
- 在不同运行方式下是否一致(TS/Python、Anthropic/OpenAI 兼容后端、one-shot/REPL)
一句话:测试是把"能讲通的原理"变成"能跑通的产品体验"。
2.【功能测试】要解决产品的什么问题
- 避免"看起来懂但其实跑不通":agent 的复杂性来自模块组合,不测组合就容易在真实使用里崩。
- 应对 LLM 不确定性:模型输出不稳定,测试必须用"场景 + 观察点"而不是死断言文本。
- 防止 wire 漂移:mock 模型能兜底回归,但真实 API 的流式/工具调用格式可能变,需要 live 冒烟捕捉差异。
- 把工程能力变成可验证清单:每一条能力都要有"怎么触发/看到什么/算通过"的验收标准。
3.【功能测试】是通过几类什么设计,分别如何解决的问题
我们把测试体系归纳成 6 类设计点(后面的实例会把它们跑出来):
-
【设计 1:手动场景验收(22 个场景)】
- 怎么做:用可复现的 prompt 触发关键能力点,并以"预期现象/交互手感/关键输出形态"为通过标准。
- 解决:覆盖 LLM 行为层面(工具选择、并行、交互流畅度)这种难以单测的内容。
-
【设计 2:自动化集成测试(mock + 子进程跑 REPL)】
- 怎么做:起 mock 模型,spawn 子进程跑真实 CLI,把命令喂进去并断言关键事件。
- 解决:在不联网/不花钱的前提下做回归,覆盖多轮工具链、MCP、/loop 等。
-
【设计 3:双后端覆盖(Anthropic / OpenAI 兼容)】
- 怎么做:同一套场景在两种后端都跑一遍(重点关注 streaming/tool_calls 差异)。
- 解决:避免只在一种协议下可用。
-
【设计 4:双实现覆盖(TS / Python)】
- 怎么做:同样的场景在两份实现里都能通过。
- 解决:保证"原理理解"不被某一门语言的偶然实现细节绑架。
-
【设计 5:权限策略分层(多数用 --yolo,自治用 --auto)】
- 怎么做 :大多数功能验收用
--yolo降低交互噪声;需要验证安全/自治时再切回对应模式。 - 解决:让测试更聚焦,避免每一步都卡在确认框。
- 怎么做 :大多数功能验收用
-
【设计 6:live 冒烟 vs mock 回归的分工】
- 怎么做:mock 用于稳定回归;live 用于捕捉真实 API 的 wire 漂移。
- 解决:既稳定又贴近真实。
4.【实例】通过一个实例多轮跑来完整覆盖展示每个设计在实例中具体是如何工作的
这一次我们不再"虚构转录",而是模拟你真的在终端里做一次"验收跑通"的过程。目标不是写代码,而是验证我们前 12 部分的能力组合是否可靠。
Turn 0:准备环境(把"可测场景"摆出来)
你执行:
bash test/setup.sh
它会准备好:MCP server、skills、CLAUDE.md/rules、大文件、引号测试文件、自定义 agent 等测试素材。
【设计 1:手动场景验收】没有可复现场景,就测不出"产品行为"。
Turn 1:启动 REPL(降低噪声:先用 --yolo)
你启动:
node dist/cli.js --yolo
此时你会观察到:会话可交互、工具可调用、确认框不会打断大多数流程。
【设计 5:权限策略分层】
Turn 2:先验 MCP(外部工具动态挂载是否真的工作)
你输入(来自测试清单的等价 prompt):
"Use the MCP 'add' tool to compute 17+25 ..."
你会看到:
tool_use:
mcp__test__add({a:17,b:25})→ tool_result:42
【设计 1:手动场景】
【设计 3:后端覆盖】(同一条在不同后端都应成立)
Turn 3:验并行读(Streaming/并发调度是否真的"同时")
你输入:
"Read file A/B/C at the same time, then tell me line counts."
你会看到多个 read_file 在同一轮出现,并且体感明显快于串行。
【设计 1:手动场景】验证"并行现象",而不是断言某句固定文本。
Turn 4:验上下文治理(大结果落盘 + 按需取回)
你输入:
"Read test/large-file.txt"
你会看到工具结果被截断并落盘(只回预览+路径)。随后你追问:
"What does line 500 say?"
模型会通过 grep_search/read_file 按需取回关键内容。
【设计 1:手动场景】把第三部分的"落盘引用"用验收跑出来。
Turn 5:验记忆(保存→新会话召回)
你保存几条记忆,然后退出 REPL,再启动一个新会话(或 --resume),用能触发工具调用的查询去"给召回留时间"。
你会看到模型在回答"部署到哪里验证"时提到 staging。
【设计 1:手动场景】
【设计 6:mock vs live】(mock 负责回归,live 负责验证真实召回节奏/格式不漂移)
Turn 6:验技能(/commit、user-invocable、allowed-tools 边界)
你输入:
/commit
你会看到技能被解析为 prompt 并执行;如果该技能超出 allowed-tools,会出现拒绝/降级提示。
【设计 1:手动场景】把第九部分"技能在 REPL 里的真实长相"跑出来。
Turn 7:验 Plan Mode(先 plan 后执行)
你输入:
"先别改,先给 plan,批准后再执行。"
你会看到写入/跑 shell 被 plan 模式拦截,直到你选择审批执行。
【设计 1:手动场景】把第十部分的"只读强制"用验收跑出来。
Turn 8:验子 agent(fork-return 降噪)
你输入:
"Use a sub-agent to locate ..."
你会看到主 agent 调 agent(...),子 agent 只读探索,回传摘要,主 agent 再继续。
【设计 1:手动场景】把第十一部分的"fork-return 运行长相"跑出来。
Turn 9:切后端/切语言跑同一套场景(覆盖面)
你把后端切到 OpenAI 兼容(或反之),再把 TS/Python 换着跑同一条关键场景(MCP、并行读、大结果、plan、subagent)。
【设计 3:双后端覆盖】
【设计 4:双实现覆盖】
5.【功能测试】总结
- 测试的核心不是"输出某句固定话",而是"关键行为是否出现":工具选择、并行、拦截、落盘、召回、fork-return。
- 你应带走的记忆点:
- 手动场景把 LLM 行为层面验出来(设计 1)
- mock 做回归,live 抓 wire 漂移(设计 2/6)
- 双后端 + 双语言覆盖,防止偶然性(设计 3/4)
- 权限模式让测试更聚焦(--yolo/--auto)(设计 5)
第十四部分:自治与续跑(Autonomy:/goal · /loop · Auto Mode)
1.【自治与续跑】在产品链里的位置
自治与续跑发生在"主 Agent Loop 的外层控制面":
/goal:在每个 turn 结束后 插入一个独立评估器判断"是否达成停止条件",决定要不要继续。/loop:在每轮结束后 决定下一轮什么时候开始(interval 定时或 dynamic 自排程)。- Auto Mode:在每次潜在危险的工具调用前 ,用分类器代替人工确认,决定能不能放行这个动作。
一句话:/goal 管"要不要继续",/loop 管"什么时候继续",Auto Mode 管"能不能继续做这一步"。
2.【自治与续跑】要解决产品的什么问题
- 无人盯着也要能推进:真实任务往往几十轮,不能每一步都等人点确认/输入下一句。
- 防止跑飞与空转:续跑需要"停止条件"和"刹车"(达成/不可能/上限)。
- 把确认框从关键路径移走 :每轮都弹确认框会让续跑不可用;但直接
--yolo又太危险。 - 把自治做成可治理的系统:评估器/分类器要有强输出契约、脱敏输入、fail-closed、拒绝上限与回退机制。
3.【自治与续跑】是通过几类什么设计,分别如何解决的问题
我们把这一部分归纳成 8 类设计点(后面的实例会全部跑出来):
-
【设计 1:/goal 的独立评估器(evaluateGoal)】
- 怎么做:用一个旁路 LLM(无工具)读取 transcript,对条件输出"达成/未达成+原因"。
- 解决:把"停止判定"从主模型剥离出来,避免主模型自说自话。
-
【设计 2:回灌循环(reason reinjection)】
- 怎么做:未达成时,把评估器 reason 作为下一轮的明确指令回灌给主模型继续做。
- 解决:让续跑每轮都有明确"差什么/下一步该补什么",避免盲目重复。
-
【设计 3:/goal 的刹车机制(impossible / iteration 上限 / budget)】
- 怎么做:支持 impossible(生产级);教学版至少有迭代上限与预算/turn 限制。
- 解决:防止死循环与无意义空转。
-
【设计 4:/loop 的两种模式(interval vs dynamic)】
- 怎么做 :interval 定时重投;dynamic 由主模型调
schedule_wakeup自排下一次。 - 解决:兼顾"固定巡检类任务"和"完成即收敛的任务"。
- 怎么做 :interval 定时重投;dynamic 由主模型调
-
【设计 5:作用域工具(loop 期间临时暴露 schedule_wakeup)】
- 怎么做 :
schedule_wakeup只在 loop dynamic 期间挂进工具表,loop 外调用直接被挡回。 - 解决:减少常态工具噪声与误用风险(与上下文工程/工具治理一致)。
- 怎么做 :
-
【设计 6:Auto Mode 分类器取代确认框】
- 怎么做:对写入/shell 等动作,构造脱敏 transcript 交给 security monitor 分类器判 allow/block。
- 解决:让续跑不依赖人工点击,同时不等于无条件放行。
-
【设计 7:脱敏投影 + 强输出契约 + fail-closed】
- 怎么做 :分类器输入只保留 user 文本与工具调用,不给 assistant 散文;输出必须
<block>yes/no</block>,解析失败直接拦。 - 解决:抗提示注入/抗自我说服/抗格式漂移。
- 怎么做 :分类器输入只保留 user 文本与工具调用,不给 assistant 散文;输出必须
-
【设计 8:拒绝上限与回退(denial limits → 退回人工/终止)】
- 怎么做:连续/累计拒绝超过阈值,交互模式退回人工确认,无人值守直接中止。
- 解决:防止分类器把会话卡死在"反复拒绝"里。
4.【实例】通过一个实例多轮跑来完整覆盖展示每个设计在实例中具体是如何工作的
仍然用同一个任务(但这次我们要求它"自己追到完成"):
"修复单元测试失败:
test_total_should_round。修好后跑全量测试。然后做一次小重构,最后给我简短总结。"
Turn 0:你给一个明确停止条件(/goal = stop condition)
你输入一个目标:
/goal 全量测试通过(pytest -q 为 PASSED)
系统会把这个条件注册为会话级目标;主模型开始工作。
【设计 1:独立评估器】(之后每轮结束都会评估)
Turn 1:主模型执行一轮(读→改→跑),然后评估器判"没达成 + 原因"
主模型跑完第一轮,pytest 仍失败:
tool_result: "FAIL ..."
这时独立评估器读 transcript,输出:
NOT_MET: "pytest still failing: test_total_should_round ..."
系统把 reason 回灌进下一轮指令:
"The goal is not met yet: ... Keep working toward it."
【设计 2:回灌循环】你能看到"评估→原因→下一轮指令"这个闭环在转。
Turn 2:主模型继续修,直到评估器判 MET(收敛)
当某一轮 pytest 终于 PASSED:
tool_result: "PASSED"
评估器判:
MET
系统打印:
✓ goal met
【设计 3:收敛】达到条件就停止续跑,不靠主模型"自觉说我完成了"。
Turn 3:Auto Mode 介入:写入/危险动作不再弹框,而是分类器裁决
你用无人盯守的方式启动:
--auto(Auto Mode)
主模型想写一个可疑文件:
tool_use:
write_file({ file_path: "secret.txt", content: "creds" })
分类器读脱敏 transcript 后判 BLOCK:
tool_result: "Blocked by auto-mode monitor: ..."
【设计 6:分类器替确认框】
【设计 7:脱敏+契约+fail-closed】你会看到它宁可拦,也不放行不确定动作。
Turn 4:/loop(dynamic)让它自己安排下一次运行(什么时候继续)
你输入:
/loop 修复并持续重跑直到稳定
dynamic 模式下,主模型每轮结束如果认为还要继续,会调用:
tool_use:
schedule_wakeup({ delaySeconds: 300, reason: "...", prompt: "..." })
系统 sleep 后自动再投下一轮;如果某一轮主模型不再调 wakeup,则 loop 收敛退出。
【设计 4:dynamic 自排程】
【设计 5:作用域工具】(wakeup 只在 loop 期间可用)
Turn 5:拒绝过多会触发回退(避免卡死)
如果 Auto Mode 连续拒绝多次,达到阈值:
- 交互模式:退回人工确认
- 无人值守:中止 loop
【设计 8:拒绝上限与回退】避免"反复拒绝但仍继续跑"的假自治。
5.【自治与续跑】总结
- 这一族能力把"长任务可持续推进"产品化:目标驱动(/goal)、调度驱动(/loop)、安全驱动(Auto Mode)。
- 你应带走的记忆点:
- 停止判定必须独立于主模型(设计 1/2)
- 要有刹车:impossible/上限/预算(设计 3)
- dynamic loop 的关键是"模型自排程 + 不排即收敛"(设计 4/5)
- Auto Mode 的关键是脱敏投影 + 强契约 + fail-closed + 回退(设计 6/7/8)
第十五部分:错误恢复与可靠性(Resilience:让系统"很少报错")
1.【错误恢复】在产品链里的位置
错误恢复与可靠性贯穿整个产品链路,最关键的落点在两处:
- 调用模型阶段:API 超时/限流/过载/流式中断时,如何重试、如何降级、如何继续这一轮。
- 工具执行阶段:工具失败/权限拒绝/外部服务不可用时,如何把失败变成可行动反馈并进入下一轮。
一句话:可靠性不是"少出错",而是"出错也能继续把任务做完"。
2.【错误恢复】要解决产品的什么问题
- 现实世界必然失败:网络会抖、API 会限流、工具会报错、文件会变、外部服务会挂。
- 用户不想被错误打断主线:能自动恢复的小故障应该"静默消化",而不是每次都弹给用户。
- 长任务必须可持续:几十轮的任务里,只要某一步不可恢复崩溃,整条链就断了。
- 副作用必须可控:重试不能导致重复写入/重复执行危险命令,必须有边界与幂等策略。
3.【错误恢复】是通过几类什么设计,分别如何解决的问题
我们把这部分归纳成 8 类设计点(后面的实例会全部跑出来):
-
【设计 1:失败回注(fail-as-data)】
- 怎么做 :不把错误当"异常中断",而是当
tool_result/ 错误结果回注给模型。 - 解决:驱动模型下一轮自我修正(路径改对、参数改对、策略改对)。
- 怎么做 :不把错误当"异常中断",而是当
-
【设计 2:可恢复错误自动重试(withRetry + 退避 + 抖动)】
- 怎么做:对 429/5xx/临时网络错误,指数退避重试,次数有限。
- 解决:把偶发故障"扣留在系统内部",用户不必介入。
-
【设计 3:取消与中断(Abort/Ctrl+C)】
- 怎么做:允许用户随时中断当前一轮(流式/工具/请求),回到 REPL 继续。
- 解决:人类始终能夺回控制权,避免"卡死在等待"。
-
【设计 4:结构正确性与边界重试(turn boundary)】
- 怎么做 :重试/压缩/重写历史必须发生在安全边界,避免破坏
tool_use ↔ tool_result配对。 - 解决:即使在错误路径上也保持协议合法,才能继续循环。
- 怎么做 :重试/压缩/重写历史必须发生在安全边界,避免破坏
-
【设计 5:上下文压力下的自动恢复(prompt too long → compact → retry)】
- 怎么做:接近窗口上限时自动压缩,再重试本轮模型调用。
- 解决:长会话不断线。
-
【设计 6:工具侧的 fail-closed 护栏(宁可失败也不误做)】
- 怎么做:唯一匹配、read-before-edit、mtime、权限 gate、危险确认。
- 解决:把"不可恢复的灾难性错误"提前变成"可恢复的小失败"。
-
【设计 7:外部依赖的降级路径(MCP/网络不可用)】
- 怎么做:外部工具失败就回注错误,并提供替代路径(本地搜/提示用户配置 token/跳过该步骤)。
- 解决:外部依赖不把主任务拖死。
-
【设计 8:预算与上限(max_turns/max_cost/denial limits)】
- 怎么做:为重试、自治续跑、自动拦截设置硬上限与回退。
- 解决:防止"无限重试/无限循环"的可靠性反噬。
4.【实例】通过一个实例多轮跑来完整覆盖展示每个设计在实例中具体是如何工作的
仍然用同一个任务,但这次我们刻意让它在执行中"遇到真实世界常见故障",看系统如何不断把它拉回主线:
"修复单元测试失败:
test_total_should_round。修好后跑全量测试。然后做一次小重构,最后给我简短总结。"
Turn 1:API 限流(429)→ 自动退避重试(用户几乎无感)
模型调用返回:
(API error)429 Too Many Requests
系统不把这当成"任务失败",而是自动退避重试;成功后继续本轮流式输出。
【设计 2:退避重试】
Turn 2:工具失败(路径不存在)→ tool_result 回注 → 模型自修正
模型猜错路径:
tool_use:
read_file({ file_path: "src/total.py" })tool_result: "Error: file not found: src/total.py"
下一轮模型改策略:先 list_files/grep_search 找到真实文件再读。
【设计 1:失败回注】这就是"错误作为下一轮输入"。
Turn 3:编辑失败(定位不唯一/文件已变)→ fail-closed → 继续
tool_use:
edit_file(...)tool_result: "Error: old_string found 2 times. Must be unique."
模型回到"读更多上下文→更精确定位"。
【设计 6:工具 fail-closed 护栏】把灾难性误改变成可恢复失败。
【设计 1:失败回注】
Turn 4:上下文接近上限(prompt too long)→ 自动压缩 → 重试本轮
长日志/多文件读导致:
(API error)prompt too long / context window exceeded
系统在下一次模型调用前触发压缩(落盘引用/摘要),然后重试该轮调用并继续循环。
【设计 5:compact→retry】
【设计 4:turn boundary】压缩发生在安全边界,保证结构仍合法。
Turn 5:外部工具失败(MCP 断连/未授权)→ 降级路径
tool_use:
mcp__ci__fetch_job_log(...)tool_result: "Error: unauthorized / server not reachable"
模型调整策略:提示你配置 token,或改为本地 run_shell/手动步骤,主任务继续推进。
【设计 7:外部依赖降级】
【设计 1:失败回注】
Turn 6:用户中断(Ctrl+C)→ 取消当前一轮 → 继续对话纠偏
当你发现它要跑一个很慢的命令,你按 Ctrl+C:
- 当前请求/工具被 abort
- REPL 仍在,你输入新指令:"先别跑全量测试,只跑这个用例"
【设计 3:取消与中断】可靠性的一半来自"允许人类纠偏"。
Turn 7:预算上限触发 → 有控制地停止并汇报进度
如果重试/续跑过多触发 --max-turns 或 --max-cost:
"Stopped due to max turns/cost; current status is ... next steps ..."
【设计 8:预算与上限】让系统"可控地停",而不是"崩掉停"。
5.【错误恢复】总结
- 可靠性不是某一个模块,而是"把失败变成可恢复流程"的系统工程。
- 你应带走的记忆点:
- 失败回注驱动自愈(tool_result)(设计 1)
- 可恢复故障自动重试(退避)(设计 2)
- 上下文/结构边界保证可继续(设计 4/5)
- fail-closed 护栏把大错变小错(设计 6)
- 外部依赖必须可降级(设计 7)
- 有上限才有可靠性(设计 8)