Coding Agent 架构设计:从 Agent Loop 到插件系统,拆解一个 AI 编程助手的核心模块

1. 概述

Coding Agent 的本质不复杂–一个在"问模型"和"执行工具"之间循环的 while 循环。但把这个循环变成每天能用的开发工具,需要一层层加上会话管理、上下文压缩、资源加载、插件系统等模块。

现代 Coding Agent 的架构核心思想是三层分离:模型适配层(屏蔽不同 LLM 的差异)、内核层(Agent Loop 运行时)、产品层(会话/压缩/资源等循环之外的事)。各层独立、松耦合,通过接口或协议通信,可以独立替换实现。

理解了 Coding Agent 的构建原理,就掌握了理解其他业务 Agent 的一把钥匙–客服、数据分析、工作流编排等业务 Agent,追根溯源都是 Coding Agent 的泛化变种。


2. Agent Loop:一切的核心

Agent Loop 是什么?一个 while 循环,在"问模型"和"执行工具"之间来回切换,直到模型给出最终答案。它不关心模型是 OpenAI 还是 Anthropic,不关心工具是读文件还是跑命令,只关心两件事:模型要不要调工具?如果要,调完继续问;如果不要,结束。

核心逻辑(简化后):

while (turn < max_turns) {
    // 1. 调模型
    assistant = model.complete(messages, tools)

    // 2. 检查是否有工具调用
    if (assistant.toolCalls().len == 0) break

    // 3. 逐个执行工具,结果 append 回 messages
    for (tool_call in assistant.toolCalls()) {
        result = tool_registry.execute(tool_call.name, tool_call.args)
        messages.append(result)
    }
}

三个关键设计点:

  • max_turns:防止模型陷入无限工具循环的安全阀。模型可能反复调用工具却始终不给出最终答案,上限是必要的。
  • 工具结果 append 回 messages:模型需要看到工具执行的结果才能决定下一步做什么。如果不把结果回写,模型就像蒙着眼睛干活。
  • 工具定义传进 complete():模型需要提前知道有哪些工具可用、每个工具的参数是什么,才能决定是否调用。

Agent Loop 还需要处理错误重试。LLM 调用和工具调用都有失败的可能,需要区分错误类型:可重试的错误(网络超时、临时限流)自动重试,不可重试的错误(参数校验失败、权限不足)直接抛出。

Agent Loop 只关心"循环",不关心消息从哪里来、执行结果存到哪里。这些循环之外的事,由产品层封装。


3. 模型适配层:统一不同 LLM Provider 的差异

模型适配层的职责只有一句话:把不同 LLM Provider 的 API 差异封装在一个统一接口后面。Agent Loop 只认这个接口,不关心背后是 OpenAI 还是 Anthropic。

统一接口需要处理的关键差异:

差异点 OpenAI Anthropic
请求体格式 messages[] 数组 content[] 数组
工具调用结构 tool_calls[] 独立字段 content[] 中的 tool_use block
流式协议 data: 行 event: 行

适配层把这些差异统一成 Message、Tool、AssistantMessageEvent 等内部数据结构。上层 Agent Loop 不需要知道"这是 Anthropic 的 tool_use 还是 OpenAI 的 function call",只关心统一后的 toolCall 内容块。

LLM 生成一个回答可能需要几秒甚至十几秒,如果等全部生成完再返回,用户只能干等。解决方案是流式输出(SSE):模型适配器在收到 SSE 的每个 chunk 时,调用 stream_callback 回调,Agent Loop 收到回调后立即通过 EventBus 发射事件,Client 收到事件后实时追加到终端显示。


4. 工具系统:Agent 的手和脚

模型适配层让 Agent Loop 可以调任何模型,工具系统让 Agent Loop 可以做任何事。

工具的定义包含四个要素:name(工具名)、description(描述,告诉模型这个工具能做什么)、parameters(JSON Schema,描述参数结构)、execute(执行函数)。

工具注册表是一个 HashMap,按工具名存储,支持三个核心操作:register(注册新工具)、definitions(返回所有工具定义,发送给模型)、execute(按名称查找并执行)。模型在每次调用 complete() 时,注册表会把所有可用工具的 JSON Schema 编码后传给模型,模型根据描述决定是否调用某个工具。

内置工具通常包括:read(读文件)、write(写文件)、edit(精确修改)、bash(执行命令)、grep(搜索)、glob(文件匹配)。扩展工具通过插件系统动态注册。


5. 产品层:循环之外的事

写一个 Agent Loop 不难,难的是把它变成每天能用的开发工具。产品层负责这些"麻烦但关键"的事情:会话管理、上下文压缩、资源加载。

5.1 会话管理(Session)

没有 Session,Agent 每次对话都是"失忆"的。存储格式采用 JSONL,每行一个独立 JSON 对象。第一行是会话头(id、创建时间、工作目录),后续每行是一条消息。

{"id":"sess_001","created_at":1717234567,"cwd":"/project","model":"gpt-4o"}
{"id":1,"parent_id":null,"timestamp":1,"role":"user","content":"帮我读 README.md"}
{"id":2,"parent_id":1,"timestamp":2,"role":"assistant","content":"我来帮你读..."}

每条消息带 parent_id,构成树状对话结构,支持分支 fork 和回滚–走错方向时可以回到之前的节点重新开一条分支。

会话恢复时逐行解析,损坏的行跳过不崩溃。这是工程上的健壮性设计–JSONL 文件可能因为进程崩溃而出现半行写入,解析时遇到损坏行应该跳过而不是整体失败。

5.2 上下文压缩(Compaction)

LLM 有上下文窗口限制,一个 Coding Agent 的对话可能持续几十轮,累积数千 Token。如果不做处理,早期消息会被窗口截断,模型"忘记"了之前的上下文。

压缩策略:当 Token 超过阈值时,把旧消息压缩成一条摘要,保留最近 N 条消息不变。

压缩前: [消息1] [消息2] [消息3] ... [消息N-10] [消息N-9] ... [消息N]
压缩后: [摘要: 之前讨论的要点] [消息N-9] [消息N-8] ... [消息N]

核心设计要点:

  • Token 预算:不引入精确 tokenizer,用字符数除以 4 近似估算。压缩决策不需要精确到个位数 Token,够用就行。
  • 触发条件:估算的消息 Token 总量超过配置的 max_tokens 时触发。
  • 摘要生成:把旧消息拼接成文本,调用模型生成摘要,替换掉原始消息。
  • 重试循环:压缩后重新执行 Agent Loop,如果仍然超限,继续压缩。

默认阈值 100K Token,保留最近 10 条消息,摘要目标长度 500 Token。

5.3 资源加载(Resources)

Resources 从文件系统加载项目规则和技能,格式化后注入 system prompt,让模型知道它有哪些工具和能力可用。

三类资源的加载顺序和优先级:

  • 项目规则(优先级从高到低):当前目录的 AGENTS.md -> 当前目录的 CLAUDE.md -> 全局配置目录的 AGENTS.md -> 全局配置目录的 CLAUDE.md
  • 技能(项目优先,同名冲突时项目级覆盖全局级):当前目录的 .agent/skills/ -> 当前目录的 .agents/skills/ -> 全局配置目录的 skills/
  • 数据结构:每个 Skill 包含 name、description、filePath、source(global 或 project)、content

SKILL.md 文件携带 YAML frontmatter,解析后构建成 XML 结构注入到系统提示词中,告诉模型当前有哪些技能可用:

<available_skills>
    <skill>
        <name>basedpyright</name>
        <description>Python static type checking</description>
        <location>/path/to/skill/SKILL.md</location>
    </skill>
</available_skills>

模型看到技能列表后,如果判断当前任务需要某个技能,就会主动读取该 SKILL.md 的完整内容。这就是渐进式披露的实践–初始只给目录,用到时再加载详情。


6. 事件系统与插件机制

Agent Loop 在跑,但外界怎么知道它跑到了哪一步?事件系统就是答案。Agent Loop 每做一件事–开始一轮、生成一个 Token、调一个工具–就往 EventBus 上发一个事件。谁关心这个事件,谁就注册回调。

EventBus 有三个回调槽(agent / session / compaction),插件安装时把原回调保存下来,换成自己的 dispatch 包装函数。执行时先执行原回调(写 socket 流式返回给客户端),再遍历所有已注册的插件,逐个调用对应的 hook 函数。

插件支持的 Hook 类型:

Hook 触发时机 可做操作
on_tool_start 执行工具前 拦截危险命令、修改参数
on_tool_end 工具执行后 修改执行结果、标记错误已处理
on_context LLM 调用前 注入系统指令
on_agent_start Agent 启动时 阻止启动
on_session_before_compact 手动压缩前 阻止压缩

插件的优势在于即插即用、动态加载。一个 bash-guard 插件可以在 on_tool_start 时拦截 rm -rf 这样的危险命令,在 on_context 时注入"使用 bash 时注意安全"的系统指令,不需要修改核心代码。


7. 网络层与客户端通信

网络层定义了 Server 与 Client 之间如何通信。核心设计:TCP + JSON line 协议,流式通信 + 全双工。

TCP 天生适合这个场景–客户端发一条消息,服务端把思考过程、工具调用、最终回答一条条推送给客户端,全双工意味着客户端可以随时发消息(比如中断请求),服务端也可以随时推送事件。

语言无关是网络层的核心价值。Server 不关心 Client 用什么语言实现,只要遵循 JSON line 协议就行。这意味着引擎层可以用追求性能的语言(如 Zig)实现,客户端可以用生态丰富的语言(如 Python)实现终端交互 UI,各取所长。


8. 总结

Coding Agent 的核心架构可以归纳为六个模块:

  • Agent Loop:核心循环,在"问模型"和"执行工具"之间切换
  • 模型适配层:统一不同 LLM Provider 的 API 差异
  • 工具系统:注册表管理工具,支持动态扩展
  • 产品层:会话管理(JSONL + 树结构)、上下文压缩(摘要 + 重试)、资源加载(规则 + 技能)
  • 事件系统:EventBus 驱动,支持插件动态加载
  • 网络层:TCP + JSON line,语言无关

各层之间通过接口或协议解耦,可以独立替换实现。理解了这套架构模式,就掌握了理解其他业务 Agent 的一把钥匙。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐