RSS

第 20 章 / 共 21 章

综合装配:很多机制,一个循环

约 7 分钟 更新于

上一章留下的问题:你现在手里有十九个机制,但它们是十九章分别讲的。真正的问题是:它们同时存在时,一次 turn 到底按什么顺序发生?

先说结论,也是全书最重要的那句话:那段 while True 一个字都没变。

第1章写下的循环——调模型、看 stop_reason、执行工具、把结果回灌——今天仍然是同一段。多出来的十九个机制,全部挂在它的四个位置上:调模型之前、调模型时、执行工具前后、准备退出时。

20.1 一次完整 turn 的时序

第20章:一次完整 turn 的时序
图 20.1:所有机制在一次 turn 中的先后位置。要看的是最中间那一段——被十几个机制包围的,仍然是”调模型 → 判断 → 执行 → 回灌”这四步。左右两侧越厚,中间那段越显得没变过。

按位置列一遍:

调模型之前

机制章做什么
UserPromptSubmit 钩子4校验输入、注入上下文
定时队列消费14把到点的任务作为 [定时任务] 消息注入
后台完成通知13把 <task_notification> 并进消息
收件箱注入15、16协议消息先被代码消费,其余注入上下文
计划提醒5连续多轮没更新清单就插一条提醒
压缩流水线8L3 → L1 → L2 →(必要时)L4
系统提示词组装7、9、10、19按当前状态拼身份、技能目录、记忆索引、已连服务
工具池装配2、19内建工具 + 已连 MCP 工具

调模型时

机制章做什么
错误恢复11429/529 退避、max_tokens 提额与续写、超长兜底压缩

执行工具时

机制章做什么
PreToolUse 钩子 + 权限3、4危险操作在执行前被挡下
后台派发13慢命令进守护线程,立刻返回占位结果
worktree 上下文18队友的文件操作落在自己的工作区
工具调度2按名字查表执行
PostToolUse 钩子4日志、超大输出预警、副作用

准备退出时

机制章做什么
Stop 钩子4清理、统计,或者强制续跑
记忆抽取9一轮自然结束时提取新事实并去重写入

20.2 骨架代码

把上面这张表翻译成代码,就是这样:

def agent_turn(messages: list, state: RecoveryState) -> None:
    while True:
        # ── 调模型之前 ────────────────────────────────
        for job in consume_cron_queue():                       # 第14章
            messages.append({"role": "user", "content": f"[定时任务] {job.prompt}"})
        for note in collect_background_results():              # 第13章
            messages.append({"role": "user", "content": note})
        for msg in consume_inbox("lead"):                      # 第15、16章
            messages.append({"role": "user", "content": f"<inbox>{msg}</inbox>"})

        messages[:] = tool_result_budget(messages)             # 第8章 L3
        messages[:] = snip_compact(messages)                   # L1
        messages[:] = micro_compact(messages)                  # L2
        if estimate_token_count(messages) > THRESHOLD:
            messages[:] = compact_history(messages)            # L4

        ctx = current_context()                                # 第10章
        tools, handlers = assemble_tool_pool()                 # 第2、19章

        # ── 调模型 ────────────────────────────────────
        response = call_model(messages, state, tools=tools,    # 第11章
                              system=get_system_prompt(ctx))
        messages.append({"role": "assistant", "content": response.content})

        # ── 判断是否继续 ──────────────────────────────
        if response.stop_reason != "tool_use":
            force = trigger_hooks("Stop", messages)            # 第4章
            if force:
                messages.append({"role": "user", "content": force})
                continue
            extract_memories(messages)                         # 第9章
            return

        # ── 执行工具 ──────────────────────────────────
        results = []
        for block in response.content:
            if block.type != "tool_use":
                continue
            blocked = trigger_hooks("PreToolUse", block)       # 第3、4章
            if blocked:
                results.append({"type": "tool_result", "tool_use_id": block.id,
                                "content": str(blocked)})
                continue
            if should_run_background(block.name, block.input): # 第13章
                bg_id = start_background_task(block)
                results.append({"type": "tool_result", "tool_use_id": block.id,
                                "content": f"已在后台启动({bg_id}),可以继续做别的事。"})
                continue
            output = handlers[block.name](**block.input)       # 第2章
            trigger_hooks("PostToolUse", block, output)        # 第4章
            results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})

        messages.append({"role": "user", "content": results})  # 第1章:回灌

把所有带注释的行遮住,剩下的就是第1章那三十行。这就是本书的全部结构性主张。

20.3 装配时才会暴露的三个问题

单看每一章都很清楚,装到一起会冒出一些只在集成时才出现的问题:

顺序敏感。 压缩必须在组装提示词之前(否则你按压缩前的状态算了 token),工具池必须在调模型之前重建(否则模型看不到新连的 MCP 工具),协议消息必须在注入上下文之前被消费(否则状态机停在 pending)。这些顺序不是风格,是正确性。

机制之间会互相削弱。 压缩会抹掉队友的身份(第17章),也会抹掉当前的任务清单(第5章);压缩之后需要主动把这两样重新贴回去。同样,切换备用模型(第11章)会让提示词缓存(第10章)全部失效。集成的工作量,很大一部分花在处理这类交叉影响上。

成本会叠加。 记忆的相关性侧查询、L4 摘要、子代理、队友——每一个都是额外的模型调用。全开的时候,一次”用户提问”背后可能有五六次模型调用。这些开销要能被观测、能被单独关掉。

20.4 不要一次装满

到这里你已经有能力实现所有二十个机制。但**“能装”不等于”该装”。**

一个实用的引入顺序,按痛点触发:

什么时候加加什么
一开始循环 + 工具调度(第1、2章)
第一次看到模型跑出危险命令权限 + 钩子(第3、4章)
第一次看到长任务跑偏计划清单(第5章)
第一次撞到上下文超限压缩(第8章);子代理和技能(第6、7章)通常同期出现
第一次因为报错整个任务白跑错误恢复(第11章)
第一次希望”关掉终端明天接着干”记忆 + 任务系统(第9、12章)
第一次因为等命令浪费大量时间后台任务(第13章)
第一次希望它自己按时跑定时(第14章)
第一次一个 Agent 真的扛不动团队 + 协议 + 自治 + 隔离(第15–18章)
第一次需要接外部系统MCP(第19章)

每一个机制都应该由一次真实的疼痛触发,而不是因为教程里有这一章。提前加上的机制会带来复杂度、成本和 bug,却没有对应的收益。这也和”从最简单的层级开始,只在明确需要时才增加复杂度”这个通行建议一致。

20.5 常见坑

20.6 给每个机制装上开关与日志

20.7 把五个机制放回它们在 turn 中的位置