从零开始的 Agent 开发:从 Agent Loop 开始
0x00 引子
从 Prompt Engineering 到 Context Engineering 再到 Harness Engineering,AI 应用的开发方向在不断革新。Claude Code 不是第一个吃螃蟹的,但是这个方向上最具代表性的产品之一——它的源码泄漏事件,更是让所有人得以一窥 Harness Engineering 的实际样貌。
我也有意写一个 Claude Code ,遂起名 letcode,以这篇文章记录。
架构有参考 Claude Code 的部分,也有俺寻思的部分,权作试错和参考。
0x01 三次范式转移
在动手写代码之前,先理清 Agent 这个概念是怎么一步一步走到今天的。
Prompt Engineering 时代,AI 辅助编程更多体现在一问一答的交互上。在网页上与 LLM 聊天,精心构建提示词引导 LLM 思考并输出符合预期的格式。这个时候的 AI 更像是咨询工具,做小部分代码生成。
Context Engineering 时代,随着项目复杂度上升,简单的对话窗口渐渐难以满足需求。人们开始意识到应当控制 AI 能看到什么、在什么时候看到什么,而不仅仅是一股脑塞给它。RAG、记忆机制、上下文裁剪等技术被引入——给 LLM 外挂一个"脑子",在合适的时候把历史信息传递给它,同时受 token 限制决定哪些该给、哪些可以舍弃。工程化的雏形已经出现。
Harness Engineering 时代,Agent 开始从“会调用工具的模型”转向完整工程系统。此时智能不只体现在 LLM 自身,也体现在工具、权限、上下文、事件、验证和运行时编排组成的整体 harness 上。Tool Call 让 LLM 可以在信息不足时自主获取信息;任务拆解让 LLM 列草稿、定计划、按步骤执行;多 Agent 编排可以让高智能 LLM 把控方向,让性价比模型执行边界清晰的重复任务,从而降低高价模型占用,或者换取更好的并行度。每一步都可审计、可验证,不合格则重做。
而让这一切跑起来的引擎,就是 Agent Loop。
0x02 最小 Agent 长什么样
一个基础的 Agent 应该能够接受输入、处理后输出、循环往复,直到满足终止条件。这就是 Agent-Loop——让模型从单次回答进化到持续决策的关键。
在 letcode 中,Agent Loop 的核心结构非常简单:
1 | |
五个字段,各司其职:
- model:模型抽象,负责生成。不是一个具体的 API 客户端,而是一个 trait。
- tool_registry:注册式工具表。Agent 能调什么工具,注册表说了算。
- tool_context:工具执行的上下文,如项目根目录在哪、属于哪个 session。
- permission_policy:权限策略。工具能不能执行、要不要确认,由它裁决。
- permission_scope:当前 Agent 的权限边界,能读哪些文件、能跑什么命令。
模型抽象
模型本身也是一个 trait,不绑定任何具体 provider:
1 | |
ModelInput 是当前轮次的上下文消息,ModelTurn 包含模型的原始响应和解析后的意图。意图只有三种可能:
1 | |
注册式工具系统
工具通过 trait 注册到 ToolRegistry,Agent Loop 不关心具体有哪些工具:
1 | |
目前 letcode 内置了六个工具:list_files、read_file、search、apply_patch、run_command、git。每次新增工具,只需要实现 Tool trait 然后注册,Agent Loop 的代码不用动。
状态管理
ConversationState 承载了 Loop 全部运行时状态:
1 | |
对话历史、每一轮的事件序列、工具调用结果等全在这里。iteration 和 max_iterations 控制循环上限,finished 和 stop_reason 标记终止状态。这个结构打算序列化到磁盘,以便下次中断恢复。
0x03 循环里面发生了什么
Loop 的核心逻辑在一个 loop {} 里。每一轮的流程如下:
1 | |
关键代码(简化):
1 | |
一些俺寻思:
1. Prompt 每轮重建。 没有一次性构建好就不管,每一轮都重新调用 PromptBuilder,注入当前的工具 schema、output contract、运行时指令和历史消息。完整渲染结果顺手做了 hash,存进 artifact 目录。
2. 工具结果回写为 User 消息。 没用 OpenAI 的 tool role,用了 Role::User。原因是想让 Agent Loop 对 provider 保持无感,换 provider 只换 AgentModel 实现,Loop 的代码不用改。
3. 权限裁决单拎一层。 收敛到 PermissionPolicy,根据工具的风险级别和当前 Agent 的权限 scope 统一裁决。
4. 所有步骤都记事件。 LoopStarted → ModelRequest → ModelResponse → ToolCallRequested → PermissionDecisionRecorded → EvidenceOperationStarted → ToolResultProduced → LoopStopped。事件同时写进 ConversationState.events 和通过 Observer 推给外部。Observer 是个 trait,目前接的是 Session Store,将来 TUI 也可以实现同一个 trait 来消费。TUI 只管渲染,不碰 Agent 内部状态。
5. 停止条件列了七种。 FinalAnswer(任务完成)、MaxIterations(超出上限)、PermissionDenied(权限被拒)、ModelError(模型调用失败)、InvalidAssistantIntent(意图解析失败)、UnknownTool(未知工具)、ToolExecutionFailed(工具执行失败)。大概是不全的,但现在能用就行。
0x04 从 Loop 到 Runtime
Agent Loop 是心脏,但仅靠它撑不起一个 Runtime。围绕 Loop 寻思了几层:
Tool Runtime。 注册式工具系统,目前内置 6 个工具(list_files、read_file、search、apply_patch、run_command、git)。每个工具带上风险级别和能力标签,给权限层用。
Permission Runtime。 按风险级别决定执行策略:低风险自动执行,中风险执行并记录,高风险先拒绝(TUI 审批流还没写)。想的是权限判断应该是 agent core 的能力,不要散落在各个工具实现里。
Event Log / Session Store。 Session 写到 .letcode/sessions/。letcode status 看最近一次,letcode resume <id> 恢复。消息、事件、工具结果、prompt snapshot 都落盘。
Execution Evidence Foundation。 工具产出的文件内容、diff、命令输出存成 artifact,散在 .letcode/artifacts/ 下面。方便后续追溯,虽然不知道用不用得到。
Prompt Builder。 Base → Role → Stage → Task → Tool Schema → Output Contract → History,分层拼 prompt。每轮的完整渲染带版本号和 hash。
0x05 接下来的路
当前 letcode 跑通了第一版。可以 CLI 单次 prompt 运行、tool call 闭环、权限裁决、session 持久化和恢复。
但离我想要的还差不少:
- Context Manager:现在给模型喂的是全部历史,token 消耗很大。后面需要做上下文裁剪、摘要和预算控制
- Workflow Engine:准备集成 workflow ,小任务轻量执行,长复杂任务走 Interview → Spec → Plan → Execute → Review → QA
- TUI:上下分栏布局,实时展示对话流、工具调用、计划状态,目标是 OpenCode —— OpenCode 的TUI真好看吧
- Subagent / Multi-agent:主 Agent 编排,子 Agent 在受限上下文中执行任务
然后还有 Git 工作流(add/commit/push,目前只做了只读)、worktree 隔离下的并行多 Agent 协作。
能不能写完不知道,总之目前能跑。