法不净空,觉无性也。

第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 自己执行自己写的代码的安全架构。要么不让它写(限制能力),要么让它随便跑(不安全)。两者之间缺少一个分级的、可审计的执行通道。

第四,也是最重要的——这些瓶颈不是独立的。 身份不持续导致无法学习;无法学习导致每次任务的评估都是新的;缺乏自扩展能力导致解决新问题的唯一途径是更新二进制。四者形成一个死循环。

于是有了三个核心设计问题,也是这本书全部内容围绕的骨架:

  1. 身份标识:agent 怎么知道自己是谁、属于哪里、有什么限制?
  2. 能力扩展:agent 如何安全地获得新能力——不是更新二进制,而是自生长?
  3. 安全边界: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_filewrite_fileglobrun_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 的第一个设计决策就是:把身份声明从常量中抽出来,放入一个文件。

这个方案的核心逻辑出奇地简单:

  1. 启动时读 AGENTS.md——这个文件写明了 agent 的角色、行为准则、不可违反的规则
  2. CONTEXT.md——这个文件写明了项目使用的领域术语及其边界
  3. 把两个文件的内容拼入 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 中立路线:定义一套内核自己的消息模型(MessageMessageItemToolCallToolResult),在内核与 provider 之间加一层协议适配。这个决策由 ADR 0017(Provider Neutral Message Model)固化。

ADR 0016(Channel Topology and Event Model)则回答了另一个早期问题:事件怎么在内核中传递。最初的原型使用单个 channel 传递所有消息——指令和事件混在一起。当取消指令被 token 事件流堵住时,这个设计被推翻,改为双通道分离。

这些早期 ADR 的共同特点是:它们不是为了增加功能而写,而是为了修复一个已经暴露的设计缺陷而写。 这是 CodeCoder 架构决策记录的一个显著特征——大多数 ADR 是对真实演进的记录,而不是"预先设计"的文档。这一点在后续的 ADR 解读中会反复出现。


本章提出全书的骨架——三个核心问题。下一章回答第一个问题:身份标识。