从零构建 Coding Agent
English
本章目录

00. 导论:Agent 不是一次 API 调用

如果你已经会调用一次大模型 API,你拥有的是一个“会回答问题的函数”。Coding Agent 要解决的是另一类问题:用户给出一个目标,系统需要反复观察环境、调用工具、解释结果、修正计划、保存进度,并在中途被用户插话、终止或恢复后继续工作。它不是更长的 prompt,而是一个围绕模型构建的运行时;这层围绕模型的工程,也可以理解为 agent harness,是 Harness Engineering 关注的核心范围。

这套教程会带你构建一个名为 tiny-agent 的教学项目。它不会复刻任何现成产品,但你在 Claude Code、Codex 这类 Coding Agent 产品中看到的许多核心能力,也建立在类似机制上;本教程主要吸收 Pi Agent 这类真实运行系统的关键设计:协议层和厂商 API 隔离,工具执行由 stop reason 驱动,会话以追加日志作为事实源,长对话通过压缩投影继续推进,读写文件和执行命令都经过安全边界,CLI、TUI、JSON 和 SDK 都消费同一条事件流。教程的每一处设计决策,都对应真实系统在生产环境里踩过的坑。

你最终要做出的东西

最终项目需要具备这些能力:

  • 接收用户任务并调用模型。
  • 暴露 readgrepeditwritebash 等工具。
  • 根据模型的 tool use 请求执行工具,并把结果回传给模型。
  • 以事件流输出模型文本、工具调用、工具进度、工具结果和 turn 结束。
  • 把所有消息和非消息事件写入 JSONL 会话日志。
  • 从任意会话恢复上下文,并在上下文接近窗口上限时压缩。
  • 支持用户在模型运行中插入 steering 或 follow-up。
  • 提供至少两种运行壳:交互式 CLI 和机器可读 JSON 模式。
  • 通过 harness 统一管理 turn snapshot、运行中变更、会话写入顺序与完成屏障。
  • 用 faux provider 和会话回放做零成本测试。

这不是一个玩具聊天机器人。每一项能力都对应真实产品里会遇到的故障:模型生成非法工具参数、两个并行工具同时改同一个文件、流式输出中断、上下文被压缩后忘记刚读过的文件、用户中途改变目标、会话恢复后工具 schema 已经变了。教程会把这些失败模式摆在前面,而不是直接给出“最佳实践”。

同样重要的是说清楚本教程不做什么,好让你把精力放在核心机制上。它不搭建云端服务,不训练或微调模型,不接入某个具体的编辑器插件协议,也不实现多 Agent 协作或分布式调度。它默认工具在本机执行,把远程沙箱和容器隔离留成一个可替换的接口,而不是一份完整实现。你要建的是一个能在本地仓库里可靠改代码的单 Agent 运行时;把这一层做扎实,之后接云、接界面、接隔离都是替换边界的事,而不是重写内核。

分层是整套教程的主线

一个能长期演化的 Agent 系统,通常会分成五层,每层只依赖下一层的契约:

  1. 协议层:统一的消息类型、工具定义、stop reason、usage 和流式事件。厂商差异被隔离在 adapter 里。
  2. Agent loop:由模型响应驱动的状态机。它执行工具、回传结果、处理错误和中止。
  3. Agent harness / 运行时内核:在 loop 之上维护状态、turn snapshot、队列、生命周期和事件结算。
  4. 产品壳:交互式终端、print 模式、JSON 模式、RPC、SDK。它们都是同一条事件流的不同投影。
  5. 扩展层:让第三方代码通过受控 API 注册工具、命令和钩子,而不是修改内核。

这个分层不是纸面架构。它决定了非常具体的问题:会话日志应该存什么格式的消息(协议层类型,而不是某家厂商的响应);测试应该 mock 哪个边界(协议层,而不是 HTTP 客户端);新增一个 JSON 输出模式要写多少代码(只写序列化,不碰内核)。教程后面的每一章都在填充这张图里的某一块。

把这张分层图套到一次真实请求上,就能看清每一层各负责哪一步。用户输入先到达运行时内核(第三层),内核构建上下文并交给协议层;协议层(第一层)的 adapter 把内部消息转换成某个 provider 的请求格式,收到响应后再转回内部 AssistantMessage 和归一化的 stop reason;Agent loop(第二层)读取 stop reason 和 tool call,执行工具、把 tool result 送回、决定是否再问一次模型;产品壳(第四层)把这一路的事件渲染成终端界面或 JSON 行;扩展层(第五层)在 tool call 前后、压缩之前、会话开始时插入自己的钩子。同一次请求,五层各自只做一件事——这正是分层想换来的确定性。

第三层也是 Harness Engineering 的主要落点。Agent loop 回答“这一次循环怎样推进”,Context Engineering 回答“下一次请求应该看到什么”,harness 则回答“这些能力怎样被装配、何时允许变化、什么时候才算真正完成”。第十五章会在全部基础组件完成后,把这套一致性契约集中落到代码和竞态测试上。

读者契约

这套教程默认你熟悉 TypeScript、Node.js、Promise、异步迭代器、命令行和基本文件系统 API。Agent 相关概念会从 tool calling、循环、事件、会话和工具边界逐步展开。示例代码会保持在教学规模:足够表达边界,不把所有边角情况塞进一段无法阅读的代码里。

每章都有三个层次:

  1. 概念:为什么需要这一层。
  2. 结构:这一层和上下游的契约是什么。
  3. 检查点:读者完成后应该看到什么行为。

如果你只想学 prompt 技巧,这套教程不合适。如果你想知道一个真正能替你改代码的系统怎样被拆成协议、循环、工具、状态、权限、界面和扩展,教程就是为这个目标写的。

成本与测试策略

Agent 开发不能把每次测试都变成真实模型调用。原因有三个:费用不可控,输出不可重复,失败时很难判断是模型问题还是运行时问题。所以教程从一开始就引入 faux provider。它不是 mock 一个函数返回字符串,而是脚本化返回完整 assistant 消息、tool use、usage 和 stop reason。这样你可以用录制好的响应测试 agent loop、工具错误回传、压缩、恢复和 UI 事件。

真实模型只用于少量端到端检查。默认开发流程应该是:

  • 单元测试用 faux provider。
  • 集成测试用会话回放。
  • 少量 smoke test 调真实模型。
  • 成本统计进入每次 assistant 消息的 usage 字段。

成本统计值得从第一天就做对。成熟系统的 usage 不只有输入输出 token,还包括缓存读、缓存写和推理 token,并按模型目录里的单价换算成金额。等到用户问“这个任务为什么花了两美元”时再补,就要回填所有历史消息了。这五类 token——input、output、cache read、cache write、reasoning——从一开始就该分字段记录,它们是第四章 provider 协议和第十六章成本核算共同依赖的事实。

这条纪律会贯穿整套教程。没有可重复测试的 Agent 很快会变成一个靠感觉调参的黑盒。

核心心智模型

可以把 Coding Agent 看成下面这条数据流:

user goal
  -> context builder
  -> provider adapter
  -> model response
  -> stop reason
  -> tool executor
  -> tool result
  -> session log
  -> next context projection

其中最重要的分离是“日志”和“上下文”。日志是事实源,记录发生过什么;上下文是投影,只是当前准备发给模型的那一部分。压缩、分支、恢复、UI 渲染、扩展记录都应该建立在日志上,而不是反过来把当前 prompt 当成系统状态。如何决定每次请求把日志投影成哪一部分上下文,正是 Context Engineering 要回答的问题,第九章会专门展开。

这个分离在真实系统里的直接后果是:会话可以是一棵树而不是一个数组(用户可以从任意历史节点分叉重来);压缩不删除任何历史,只是改变投影规则;同一份日志可以同时驱动终端渲染、HTML 导出和测试断言。教程会在会话日志和压缩两章里把它落到代码。

本章检查点

读完这一章后,你应该能用一句话说明 Agent 和一次 LLM API 调用的区别:Agent 是围绕模型响应构建的可恢复运行时。它的难点不在“让模型说什么”,而在“当模型要行动时,系统怎样可靠地执行、记录、反馈和继续”。