第1章 自主 AI agent 的困境
2026.07.29单轮对话可以、多步任务做不完、不会从经验中学习、无法安全自扩展——这不是 agent 的能力问题,是架构问题。
模式层
1.1 当前 agent 系统的共同痛点
从 2023 年到 2025 年,AI agent 从一个展示概念变成了被广泛使用的工具。你可以让 agent 写代码、搜文档、操作数据库。但如果你让 agent"部署这个模块,然后验证线上运行正常,如果不正常就回滚"——大部分 agent 会在第二步卡住。
这不是 LLM 的能力问题——模型足够聪明。这是架构问题:agent 缺乏一个能支撑长周期任务的运行时。
具体来说,当前的 agent 系统普遍面对四个瓶颈:
第一,单轮对话可以,多步任务做不完。 一个任务涉及五步操作,每一步都依赖前一步的输出。Agent 在第三步时上下文已经包含了前两步的操作日志和结果,但在第四步时——如果第三步骤的工具调用返回了大量数据——前两步的内容可能被窗口淹没。这导致 agent"忘记"了用户最开始的约束条件。不是模型记忆力差,是操作历史的增长淹没了原始意图。
第二,不会从经验中学习。 每个 session 都是独立的。你昨天告诉 agent"项目用 nightly Rust",今天 agent 又用 stable Rust 的特性写代码——因为它不记得昨天的对话。Session 持久化可以保存对话历史,但"用户偏好"这种结构化的、需要跨 session 检索的信息,不能靠翻聊天记录来获取。
第三,无法安全地自我扩展。 你让 agent"帮我把这个流程写成一个可复用的脚本"——它写好了,但你怎么执行它?大部分 agent 系统没有让 agent 自己执行自己写的代码的安全架构。要么不让它写(限制能力),要么让它随便跑(不安全)。两者之间缺少一个分级的、可审计的执行通道。
第四,也是最重要的——这些瓶颈不是独立的。 身份不持续导致无法学习;无法学习导致每次任务的评估都是新的;缺乏自扩展能力导致解决新问题的唯一途径是更新二进制。四者形成一个死循环。
于是有了三个核心设计问题,也是这本书全部内容围绕的骨架:
- 身份标识:agent 怎么知道自己是谁、属于哪里、有什么限制?
- 能力扩展:agent 如何安全地获得新能力——不是更新二进制,而是自生长?
- 安全边界:agent 在自修改时怎么不越界——谁批准、谁审计、谁叫停?
1.2 Agent 内核的三种范式
要回答这三个问题,先从底层看:agent 的运行内核应该是什么结构?
自 2023 年以来,业界的 agent 系统大致经历了三种内核范式:
REPL 循环——最朴素的起点。
loop {
input = read_user()
think(input)
output = act()
print(output)
}
读输入、想、操作、输出——四个步骤,循环往复。这是几乎所有 agent 系统的起点。优点很明显:执行路径完全确定,调试只需要看输入和输出。你永远知道 agent 在哪一步。
但 REPL 循环有三个固有的局限。第一,操作阻塞期间,系统是完全冻结的——你不能在中途取消一个 30 秒的构建命令。第二,跨回合的上下文拼接需要显式处理——回合结束时输出的内容不会"自然"流入下一回合。第三,不支持任何形式的并行——一个 agent 同时在两个任务上工作是不可能的。
REPL 是单轮问答或短任务的正确选择。你的 agent 如果只做"用户问 → agent 答"这种场景,不需要升级到更复杂的范式。但一旦任务超出这个范围,REPL 就不够了。
事件驱动——可中断、可嵌套。
事件驱动范式将内核从"同步循环"变为"事件循环"。内核不再阻塞等待用户输入——它监听多个消息来源(用户、系统、工具执行结果),响应不同类型的消息。
CodeCoder 的双通道拓扑是这种范式的体现。命令通道只传递用户意图(新消息、关闭、取消),事件通道传递流式增量和结构化状态(token 增量、工具调用开始、工具调用结束)。两个通道的分离确保高优先级指令(取消)不会被低优先级事件流阻塞。
事件驱动最大的进步是协作式取消。用户按 Ctrl+C 时,系统不杀线程——它翻转一个共享 CancelToken。agent 的工具执行循环在安全点检查这个 token,如果被翻转就优雅终止,返回"已取消"状态。子 agent 也可以在同一事件通道架构下存在——父 agent 通过 agent 工具 spawn 一个只读的 AgentLoop 实例,两个 agent 共享同一套事件分发机制。
事件驱动范式支撑了多步任务和子代理嵌套——这是大多数 agent 系统应该采用的基础范式。它的局限在于 turn 内的工具执行仍然是串行的,不支持真正意义上的并行。这不是缺陷——是有意保持"一个 turn 逻辑的确定性"。
微型操作系统——多任务、常驻服务。
当 agent 需要 daemon 化运行(监听 socket、接受多客户端连接、在后台跑持久化服务)时,事件驱动范式又不够了。这时微 OS 范式进场:OS 线程 + channel,取代 async runtime。
CodeCoder 没有选择 tokio 或 async-std。原因有三:工具调用常常是阻塞式系统操作(读文件、跑命令),在 async runtime 里阻塞会卡住整个事件循环;子 agent 需要独立阻塞,OS 线程天然支持一个线程在 channel receive 上阻塞、其他线程继续运行;取消路径在 OS 线程中更清晰——共享 CancelToken,没有 abort() 或 drop() 隐含的不确定性。
多线程在 CodeCoder 中主要集中在内核基础设施层:主 agent 线程处理 turn 逻辑、工作线程推进 headless 里程碑、daemon 线程监听 Unix socket、compaction 线程周期性地压缩上下文。主 turn 内工具执行仍是串行的——这保持了"一个时刻 agent 只做一件事"的可预测性。
三种范式不是技术进步史——它们相互共存,各有用处。选择标准不是"哪个更新"而是"你的问题的复杂度需要哪种级别的隔离"。
1.3 三个核心问题 → 全书的骨架
前文提出的三个核心问题——身份标识、能力扩展、安全边界——贯穿了技术卷的全部五编。
身份标识是第 1 编哲学基石的核心。第 2 章"文件系统即自我"完整展开:为什么 identity 应该来自文件、五个身份文件各自负责什么、运行时加载 vs 编译时注入的信任含义。
能力扩展是第 3 编自我进化的主题。第三编的四章(第 6-9 章)覆盖了 Tool/Skill/Capability 三分架构、Skill 的草稿晋升机制、Capability 的环境与生命周期、以及自撰安全回路。
安全边界不是某一编的专属——它是一条贯穿线。第 4 章(工具体系与权限模型)处理工具调用层面的安全,第 9 章(自撰安全回路)处理自修改层面的安全,第 11 章(客观验收门)处理输出质量层面的安全。
这条骨架是全书设计的起点。每一编的每一章都回应这三个问题中的至少一个。
案例层
1.4 CodeCoder 的起源
CodeCoder 最初不是一个计划中的项目——按照本书对设计动机的教学重组叙述,它是对使用其他 agent 系统时累积挫败感的回应。
设想这样一个典型场景(教学重组案例,非特定项目实录):2025 年前后做一个多模块重构项目,涉及五个 crate 的接口调整。所用的 agent 在每个 session 里表现得很好——但它不记得前一天说过的约束。"这个模块的公共接口不要动"——每次新 session 都要说一遍。更糟糕的是,如果任务需要六步以上,agent 在第五步时会忘记第一步的上下文。
这个场景下的权宜方案是:每轮对话都把完整的项目约束写进系统 prompt。prompt 越来越长,越来越不可维护。由此自然会想到一个问题:如果 agent 能自己读一个文件来知道"我是谁"、"我的限制是什么",而不是每次由用户在 prompt 里拼写,会怎样?
这就是"文件系统即自我"的萌芽。
原型的第一个版本是一个 REPL 循环,支持四个工具:read_file、write_file、glob、run_command。它没有任何权限系统,没有任何 session 持久化,没有任何并发能力。但它验证了一个关键假设:一个能读文件、写文件、跑命令的 agent 可以完成大多数工程任务。
瓶颈很快暴露。第一个瓶颈是 REPL 循环的取消问题——agent 执行一个 10 秒的构建时,终端完全冻结。第二个瓶颈是上下文管理——一个多小时的任务产生了大量工具调用日志,上下文迅速膨胀。第三个瓶颈是能力边界——要让 agent 自己写脚本并执行,需要在"写"和"执行"之间设闸门。
CodeCoder 从原型到生产系统的演进,就是逐一解决这些瓶颈的过程——从 REPL 到事件驱动,从无权限到四级权限模型,从平面工具集到 Tool/Skill/Capability 三分架构。
1.5 项目全景
写作时的 CodeCoder 规模如下:
| 指标 | 数值 | 说明 |
|---|---|---|
| 源文件 | 31 个 | src/、daemon/、client/ |
| 内置工具 | 26 个 | 从 read_file 到 generate_capability |
| 测试用例 | 481 个 | 含 1 个 #[ignore] |
| 架构决策记录 | 31 份 | docs/adr/ |
| 技能 (Skill) | 6 个 | skills/ 目录 |
| 能力 (Capability) | 1 个 | capabilities/ 目录 |
架构拓扑如下(简化的逻辑结构):
graph TB
subgraph "客户端层"
CC[cc client<br/>终端 TUI]
CI[CI 脚本]
BG[headless runner]
end
subgraph "Daemon 层"
LISTENER[Unix socket listener]
CMDS[cmd_tx<br/>命令通道]
EVTS[event_rx<br/>事件通道]
AGENT[AgentLoop<br/>事件驱动内核]
REG[Registry<br/>skills/ capabilities/]
end
subgraph "工具执行层"
TOOLBOX[Tool executor<br/>26 个工具]
SUB[子 agent<br/>只读实例]
end
subgraph "外部资源"
FS[文件系统]
SHELL[Shell 进程]
GIT[Git]
WEB[Web / GitHub]
end
CC -->|socket| LISTENER
CI -->|socket| LISTENER
BG -->|环境变量| LISTENER
LISTENER --> CMDS
LISTENER --> EVTS
CMDS --> AGENT
AGENT --> EVTS
REG -->|system prompt 注入| AGENT
AGENT -->|工具调用| TOOLBOX
AGENT -->|子 agent 创建| SUB
TOOLBOX -->|read_file| FS
TOOLBOX -->|run_command| SHELL
TOOLBOX -->|commit| GIT
TOOLBOX -->|web_search| WEB
SUB -->|只读工具| FS
SUB -->|只读工具| WEB
style CC fill:#e1f5fe
style CI fill:#e1f5fe
style BG fill:#e1f5fe
style LISTENER fill:#fff3e0
style AGENT fill:#f3e5f5
style TOOLBOX fill:#e8f5e9
style REG fill:#fce4ec
Agent 内核是事件驱动 + OS 线程。每个 turn 从用户消息开始,经过 LLM 调用 → 解析工具调用 → 执行工具 → 返回结果 → 循环直到 agent 自主决定输出完成。工具是串行执行的,但多个 agent 实例(如 headless runner + 交互式 session)工作在不同的 OS 线程中。
1.6 "文件系统即自我"原则的首次引出
在原型阶段,agent 的"身份"是硬编码在代码常量中的——类似于许多其他系统 prompt 的写法。每次启动,agent 被告知"你是一个有用的助手"。这个身份在 session 之间不持久,也无法被用户修改或审计。
从"每次 session 都要告诉 agent 约束条件"的这个痛点出发,CodeCoder 的第一个设计决策就是:把身份声明从常量中抽出来,放入一个文件。
这个方案的核心逻辑出奇地简单:
- 启动时读
AGENTS.md——这个文件写明了 agent 的角色、行为准则、不可违反的规则 - 读
CONTEXT.md——这个文件写明了项目使用的领域术语及其边界 - 把两个文件的内容拼入 system prompt——agent 在每次 turn 开始时都"知道"自己是谁
这意味着用户可以通过修改这两个文件来改变 agent 的行为。不需要重新编译,不需要重启守护进程——改文件、触发热加载、下一轮生效。
这个简单的决策衍生出了更多的身份文件:skills/(程序性知识)、capabilities/(可执行产物)、memory/(跨 session 记忆)。五类文件、五种不同的"自我组成部分"——这就是"文件系统即自我"原则的全部内容。
这个原则将在第 2 章中完整展开。这里只需要记住它的出发点:agent 的身份不该是一个 API 响应里的字符串常量,而应该是一组磁盘上可审计、可修改、可版本控制的文件。
ADR 深度阅读
被否的方案:绑定单 provider 路线
在 CodeCoder 的早期设计阶段(ADR 0015 之前),有一个曾被认真考虑的选项:直接绑定一个 LLM provider 的消息格式。具体来说,就是用 OpenAI 的 Chat Completion API 格式作为内核消息模型,不走中间抽象层。
支持理由:
- OpenAI 的 API 格式是事实标准——大多数 agent 项目都基于它
- 减少一层抽象 = 减少一层转换开销和调试复杂度
- 如果只用一个 provider,不需要 provider 中立
被否的理由:
- 内核一旦绑定 OpenAI 格式,切换到 Anthropic、Google 或其他 provider 时需要大面积修改内核代码
- provider 的 API 格式在快速演化——如果 kernel 绑定得太紧,provider 的变更会迫使内核变更
- 多 provider 支持在 agent 系统中不是"锦上添花"——是"容错能力"。当一个 provider 的 API 不可用时,可以切换到另一个
最终选择走 provider 中立路线:定义一套内核自己的消息模型(Message、MessageItem、ToolCall、ToolResult),在内核与 provider 之间加一层协议适配。这个决策由 ADR 0017(Provider Neutral Message Model)固化。
ADR 0016(Channel Topology and Event Model)则回答了另一个早期问题:事件怎么在内核中传递。最初的原型使用单个 channel 传递所有消息——指令和事件混在一起。当取消指令被 token 事件流堵住时,这个设计被推翻,改为双通道分离。
这些早期 ADR 的共同特点是:它们不是为了增加功能而写,而是为了修复一个已经暴露的设计缺陷而写。 这是 CodeCoder 架构决策记录的一个显著特征——大多数 ADR 是对真实演进的记录,而不是"预先设计"的文档。这一点在后续的 ADR 解读中会反复出现。
本章提出全书的骨架——三个核心问题。下一章回答第一个问题:身份标识。