第三部分

综合

把前面的知识串起来:一个完整系统的设计,一个对照 pi 的动手项目,一份速记清单。

第 17 章 核心

综合设计:企业内部的 Coding Agent 平台

场景:为公司 5000 名研发提供 coding agent 平台,在 Web 和 IDE 里提交任务(修 bug、写测试、代码审查),agent 在隔离环境里改代码并提交 MR。按“需求 → 架构 → 核心流程 → 深挖”展开。

基础需求与估算

  • 功能:提交任务、实时进度、中途插话或中止、审批敏感操作、产出 MR、查看历史。
  • 规模:高峰 10% 同时在用,约 500 个并发任务;单任务 5–30 分钟、20–80 个 turn。
  • 非功能:代码不外泄、任务互相隔离、成本可控、可审计、厂商故障可降级。

进阶架构

客户端Web · IDE 插件 · CLI,SSE / WebSocket 接收事件
↓
API 网关SSO 鉴权 · 限流 · 租户识别
↓
任务服务创建任务 · 会话级单写者 · 任务队列 · 审批流
↓
LLM 网关多厂商路由 · 重试降级 · 预算计费 · 粘性路由保缓存
Agent Runtime 集群无状态 worker 跑 agent loop;从事件日志恢复;按 turn 持久化;发事件
工具执行层沙箱池(每任务一个容器,预热)· Git 工作区 · MCP 网关 · 权限钩子
↓
会话存储只追加事件表(按会话分区)+ 对象存储
事件总线Kafka → 推送 · Trace · 指标 · 审计
离线评测回放失败案例 · 回归 · 进 CI
  1. 提交任务 → 任务服务写会话、入队。
  2. Runtime worker 领取任务(带租约),从预热池拿沙箱,拉代码。
  3. 循环:经 LLM 网关请求模型 → 工具调用发到沙箱执行 → 每步追加事件日志并发布到事件总线 → 推送服务转给前端。
  4. 敏感操作触发审批:任务持久化后挂起,释放 worker;沙箱按等待时长决定保留还是先快照再释放(见下表)。
  5. 完成后在沙箱跑测试,通过则提交 MR,释放沙箱。

深入值得深挖的点

问题思路相关章节
worker 挂了租约过期由其他 worker 接手,重放事件日志;悬空调用按幂等规则处理10
等审批占资源吗不必占 worker:持久化后释放,审批结果作为事件重新入队。但沙箱、磁盘、进程、租约仍要管:短等待保留沙箱(设租约和过期时间);长等待先把工作区快照(未提交的修改打成 patch 或推到内部 WIP 分支,记下镜像和依赖版本),再释放沙箱,恢复时新建沙箱并应用快照;后台进程(如启动的测试服务)不能跨快照保留,恢复后要重新拉起。审批超时自动拒绝并通知10、13
沙箱冷启动慢预热池;按语言 / 仓库准备基础镜像;依赖缓存—
代码和凭据安全沙箱默认断网只放行制品库;凭据不进沙箱,由执行层代理;全量审计13
成本按用户 / 团队预算;前缀缓存 + 粘性路由;小模型做摘要;缓存预热按期望收益决策6
厂商故障网关熔断降级;跨厂商切换时处理 thinking 块;降级组合提前评测3、14
并发写同一文件沙箱内按文件串行;同一会话单写者7、10
前端断线事件带序号,重连时从事件日志补发11
效果怎么衡量MR 合并率、人工修改量、测试通过率;离线评测回归15
可扩展性团队通过插件注册内部工具和审批规则,核心不改;插件钩子失败即拦截12
review 跟不上agent 产出的 MR 远多于人能审的量:合并前自动跑测试、类型检查、LLM 审查;按风险分级,低风险改动轻审,核心模块重审9、15
内部系统怎么接平台侧提供远程 MCP 网关(统一鉴权、审计);常用流程写成 skill 发布到团队仓库8、16
长任务跨会话任务服务保存功能清单和进度文件,worker 换了也能从 git 和进度文件接着干9
换个场景客服 agent、数据分析 agent、运维排障 agent 都能套用这套架构。变的主要是工具层(业务 API、SQL、监控查询)和安全策略(客服防越权查询他人数据,运维防误操作生产),循环、会话、事件、评测的设计基本不变。
前瞻 · 本章
  • 已有证据这套“提交任务 → 云端沙箱 → 产出 PR”的形态已是主流产品形态:Codex 云端任务、GitHub Copilot coding agent(均为 2025-05)、Claude Code 网页版(2025-10)都是这样。
  • 作者判断平台的瓶颈从“生成”转向“验证和 review”。设计时把自动验证和审查流程当作一等组件。
  • 作者判断工程师的角色转向写需求、设计验证环境、审查结果(OpenAI 的 harness engineering 实验就是这个方向)。平台要支持一个人同时派出、追踪、验收多个 agent。
  • 作者判断这张架构图里,“Agent Runtime”这一格的代码会变少(脚手架变薄),而“工具执行层”“会话存储”“离线评测”这几格会越来越重。
第 18 章 动手

动手项目:用 Java 实现一个 mini harness

每一步都对照 pi 的对应源码。做完之后,你对每个设计点都有亲手处理过的理解。测试从第 1 天就开始:先写一个脚本化的假模型(第 15 章),之后每加一个功能,就补上对应的单测(截断不执行、异常不中断、恢复后不重复写……)。第 10–11 天只是在此基础上加真实模型的评测集。

第 1–2 天

最小 loop

HttpClient + Jackson 直接调一家模型 API,实现 read_file、run_command,跑通第 4 章的循环;同时写好脚本化假模型和第一个单测,并给每个任务写下成功标准。对照:ai/README.md Quick Start、agent-loop.ts。

第 3–4 天

护栏

最大轮数、参数校验、两阶段执行、超时杀进程树、头尾两种截断、危险命令拦截。对照:executeToolCallsParallel、bash.ts、truncate.ts。

第 5 天

edit 工具

实现 oldText → newText 替换,含模糊匹配、唯一性检查、多处修改、按文件加锁。对照:edit-diff.ts、file-mutation-queue.ts。

第 6–7 天

会话与压缩

PostgreSQL 只追加事件表,支持恢复和分支;token 估算 + 结构化摘要压缩,不在 toolResult 处切。对照:session-manager.ts、compaction.ts。

第 8–9 天

服务化

Spring Boot:提交任务、SSE 推送(带序号续传)、中止、steering;OpenTelemetry 埋点;记录缓存命中率。对照:docs/json.md、agent.ts。

第 10–11 天

评测

补齐边界单测(steering 位置、恢复);20 个任务的评测集,每个跑 3 次,每次运行前重置仓库和沙箱、固定模型和预算,算 pass@1、pass@3 和 pass^3。这个规模适合练手和发现明显问题,几个点的差异可能只是波动(第 15 章)。对照:fauxProvider。

第 12–14 天

插件与总结

用 SPI 实现工具注册和 tool_call 拦截(失败即拦截);写一篇“我的实现 vs pi”的设计取舍总结。

选做

消融与长任务

换两个不同厂商的模型,逐个关掉 system prompt 里的提醒和额外工具跑评测,记录哪些脚手架对哪个模型有用(第 1、15 章);再给一个需要多次会话的任务加上功能清单 JSON + 进度文件 + git 提交的接力机制(第 9 章)。这两项最能说明你理解“harness 会怎么变”。

进阶总结模板(STAR)

总结模板STAR
背景:为了搞清楚 coding agent 的内部原理,用 Java 从零实现了一个 agent harness,并逐模块对照开源项目 pi 的源码。
任务:工具调用循环、edit 工具、会话持久化与恢复、上下文压缩、流式推送、安全拦截、评测。
行动:
  - 循环:两阶段执行(串行校验与审批、并行执行),超时杀进程树,截断响应不执行……
  - edit:精确 + 模糊匹配、唯一性检查、按 realpath 加锁防丢失更新……
  - 会话:只追加事件表 + 树结构,按 turn 边界恢复,悬空写操作用执行端认得的幂等键去重或先查外部状态……
  - 上下文:结构化摘要、不在 toolResult 处切,前缀保持稳定,缓存命中率从 X% 到 Y%……
结果:20 个任务 × 3 次,pass@1 = X%,pass^3 = Y%;改进工具错误信息后,有 N 个任务从失败变为稳定通过;每个合格任务的平均费用下降 W%。
反思:正则拦截不是安全边界;重试只应放在理解语义的那一层;……

数字一定要真实测出来,别人会顺着每一个数字追问。样本只有 20 个任务时,用“几个任务从失败变成通过”来描述改进,比报一个百分点的提升更诚实。

第 19 章 复习

速记清单与自测

先记骨架

七个问题动手、上下文、状态、连接、分工、验证、防护,各来自主流模型的一条工程约束;是导航分类,不是严格划分
方案五个位置实现载体:提示词、harness 代码、模型权重;规范或交付方式:开放标准、厂商 API。两个维度,一个方案可以同时占几格
什么会练进模型能示范或能被程序判对错的行为,更有机会被训练进模型,删不删看评测;执行、状态、权限、审计留在外面
新词三问回答哪个问题?方案放在哪里?和最像的老词比换了哪个维度?
约束从哪来来源各不相同:推理接口(无状态)、架构与预算(窗口有限、前缀可复用)、训练目标(会自信地错)、输入表示(指令和数据混在一起)
SFT vs RL照示范学 vs 自己试、按分数学;写得出答案用前者,判得出好坏用后者
反馈 × 更新RLHF / RLAIF / RLVR 是反馈从哪来,PPO / GRPO 是怎么更新,两个维度
harness 即训练环境所以编辑格式绑定模型,模型在训练时用惯的 harness 里往往表现更好
按需加载四维放什么、常驻还是按需、谁决定、怎么找;AGENTS.md、Skills、tool search、RAG 都在这张表里
一个验证器四个用处运行中自查、离线评测、写回 skill、训练奖励;盲区也四处共享

再记细节

AgentLLM 在循环中自主选择并调用工具,直到完成目标
Workflow vs Agent控制流由代码决定 vs 由模型决定;能用 workflow 就别上 agent
Function calling模型输出动作请求(常见是 JSON 参数,也可以是受语法约束的文本),harness 校验、执行、回填
stopReason=length参数可能截断,整批工具调用不执行
错误回填参数错、工具失败都作为错误结果交回模型,不中断循环
两阶段执行串行做校验与审批,再并行执行,按源顺序回填
两层循环内层:工具 + steering;外层:follow-up
上下文五手段截断、渐进披露、压缩、清旧结果、子 agent
压缩切分保留最近 keepRecentTokens;永不在 toolResult 处切
Prompt caching前缀一致才命中;稳定内容在前,只追加,增量表达变化
edit文本替换防行号漂移;模糊匹配;要求唯一;按 realpath 加锁
截断方向文件保留头,命令输出保留尾;完整内容落临时文件
RAG vs 搜索经典 RAG 预建索引、固定检索,快、适合海量文档 vs 模型现场多轮搜原文,慢、贵,但能精确匹配、读到最新
检索管线切块带地址;BM25 + 向量用 RRF 合并;候选再 rerank;正确、有据、够新分开评
多 agent 三类失败分派含糊、上下文被淹没、各自完成拼不起来;完成要在“合”的那层验证
多 agent收益是隔离和并行;pi 用子进程实现
会话只追加树;compaction、context_edit 也是追加
崩溃恢复turn 边界持久化;悬空写操作靠执行端认得的幂等键或查外部状态;toolCallId 本身不保证对方去重
插件契约钩子失败即拦截;状态放 details 跟随分支
间接注入读外部内容的 LLM 应用都会遇到,RAG 问答也不例外;agent 的额外风险是注入变成真实动作
致命三要素私有数据 + 不可信内容 + 对外通信,至少切一条
安全边界沙箱和权限才是边界,正则和提示词不是
重试分层只在理解语义的层重试,避免重试相乘
测试 vs 评测假模型测 harness;评测集测效果,pass@1、pass@k 与 pass^k;重置环境、固定预算、小样本别信几个点
三个 engineeringprompt ⊂ context ⊂ harness,逐层扩大,不是替代
harness 变薄?替模型思考的变薄,替模型做事、把关的变厚,部分转给模型和 API
MCP 现状成了基础设施;热点转向 tool search、code mode、skills,它们多在解决怎么用好 MCP
Skills vs MCPskill 讲方法,MCP 给能力,互补;本地 coding agent 常用 CLI + skill
长时任务功能清单 JSON + 进度文件 + git;每个会话做一项、验证、提交
编辑格式和模型绑定(apply_patch / str_replace),多厂商 harness 按模型选
造词年表搬到了首页名词按“回答哪个问题”归类后放在首页的本质地图里,切到“按年份”就是原来的年表。复习时先盖住每个词后面那句“本质”,自己说一遍。自测题已经分到各章末尾,读完一章就做那一章的。
规律:已练进模型的多是“替模型思考”的技巧;已成标配的多是协议和计费机制,讨论变少恰恰说明它们已经普及;已淘汰的多是太重的抽象或超前于模型能力的设想。

延伸阅读

  • Anthropic:Building Effective Agents;Effective Context Engineering for AI Agents;Effective harnesses for long-running agents;Code execution with MCP
  • OpenAI:Harness engineering: leveraging Codex in an agent-first world(2026-02)
  • Manus:Context Engineering for AI Agents: Lessons from Building Manus(2025-07)
  • MCP 2026-07-28 规范说明:blog.modelcontextprotocol.io;Agent Skills 规范:agentskills.io
  • Mario Zechner(pi 作者):MCP vs CLI
  • METR:Measuring AI Ability to Complete Long Tasks(2025);Chroma:Context Rot(2025)
  • ReAct: Synergizing Reasoning and Acting in Language Models(2022)
  • 后训练:InstructGPT(Training language models to follow instructions with human feedback,2022);Tülu 3(2024);DeepSeek-R1(2025)
  • 检索:LightRAG(2024);Microsoft GraphRAG(2024)
  • Simon Willison:The lethal trifecta for AI agents
  • MCP 规范:modelcontextprotocol.io
  • pi 源码:github.com/earendil-works/pi,重点文件见附录
附录 参考

pi 源码地图

pi 的代码在 earendil-works/pi(原 badlogic/pi-mono),TypeScript 编写。先建立地图,各章“pi 的做法”里的源码引用才能对上位置。本手册核对的是 commit cdaf6c2(2026-10-11)。文中的路径多是缩写,省略了 packages/、coding-agent/src/ 这类前缀,按下面的地图补全即可。建议现在就 clone 一份并切到这个版本:git clone https://github.com/earendil-works/pi && git -C pi checkout cdaf6c2。之后的版本可能改名或挪位置,对不上时以这个 commit 为准。

基础四层结构

packages/coding-agent
终端里的 pi:内置工具、AgentSession、会话存储、压缩、扩展、skill、四种运行模式
≈ 应用层
packages/agent
Agent 与 agentLoop:循环、事件、队列、钩子。核心代码约 2500 行,最值得精读
≈ 运行时引擎
packages/tui
终端 UI,差量渲染
≈ 视图层
packages/ai
统一的多厂商 LLM 接口:消息类型、流式事件、工具定义、用量与费用
≈ JDBC 驱动层
Anthropic · OpenAI · Google · 本地模型 …
模型厂商 API
≈ 各家数据库

进阶一条消息的旅程

你在终端里敲下“修一下这个测试”并回车,代码依次经过这些地方:

  1. coding-agent/src/main.ts → modes/解析命令行,选择交互 / print / json / rpc 模式
  2. core/agent-session.ts · AgentSession.prompt()展开 prompt 模板、处理扩展命令、检查是否需要压缩,把用户消息交给 Agent
  3. core/system-prompt.ts按 section 拼出 system prompt:角色、工具清单、规则、AGENTS.md、skill 索引、cwd
  4. agent/src/agent.ts · Agent管理状态和 steering / follow-up 队列,调用 agentLoop
  5. agent/src/agent-loop.ts · runLoop()循环:transformContext → convertToLlm → 流式请求 → 执行工具 → 回填结果
  6. ai/ · models.stream()把统一格式转换成具体厂商的 HTTP 请求,把 SSE 响应转换回统一事件
  7. coding-agent/src/core/tools/*.tsread / bash / edit / write 的实际实现
  8. core/session-manager.ts每条消息追加写入 JSONL 会话文件
  9. core/extensions/沿途在各个事件点调用扩展的处理器(可以拦截工具、改写上下文)

深入读源码前的 TypeScript 速查

JavaTypeScript / Node
CompletableFuturePromise + async / await;Promise.all ≈ allOf
IteratorAsyncIterable,for await (const ev of stream) 消费流式事件
sealed interface + switch可辨识联合:switch (event.type)
Bean Validation / JSON SchemaTypeBox:Type.Object({...}) 同时得到 TS 类型和 JSON Schema
SPI / ServiceLoaderexport default function (pi) {...}
Thread.interrupt()AbortSignal,层层传给工具
a != null ? a.b() : nulla?.b();x ?? [] 是默认值
单线程 + 事件循环Node 的 JavaScript 默认跑在单个主线程上,没有多线程抢同一段代码;但 await 之间仍会交错,所以 pi 照样需要“文件修改队列”(第 7 章)。用了 worker_threads 或多进程,还要另外处理并发