文章
从 0 到 1 搭建自己的 Coding Agent(一):先看清一个 Coding Agent 由哪几层组成

用了一阵 Claude Code、Codex,或者 Grok Build、Pi,很多人都会冒出一个想法:我也想自己搭一个。
2025 年 4 月我也是这么开始的。公开仓库里最早是 nezha,接着做了 TypeScript 写的 blade-code,后来才是现在这个跑在终端里的 Orca。做 Orca 时切到了 Rust,主要是为了沙箱,也看重性能。那时候官方的 DeepSeek Harness(dsh)还没出,我也没有用着顺手的 DeepSeek agent,所以自己写了一个。
现在这几家都已经很成熟了。如果你只是缺一项能力,先看能不能做成插件,或者接到现成的 harness 上。这个系列写给想把原理跑通的人。自己搭一遍,是为了看清现在大家都在讲的 harness,也就是模型外面那一整套:循环、工具、权限、上下文、验收。第(二)篇会专门写它。对照源码我会用自己的 Orca(github.com/echoVic/orca-agent),当前发布是 v0.5.19。
它由哪几层组成
我后来给它的定义很短。Coding Agent 是模型,加上工具,加上循环,再加上运行时约束。
你给它一个任务,模型决定下一步,去调工具,把结果喂回来,再决定下一步,直到任务结束,或者被你拦住。运行时约束说的是:工具能碰到哪、上下文怎么管、怎样才算做完。只在聊天窗上加一层文件读写,循环转不起来,路径限制和验收也没地方挂。
底层是模型,负责决定下一步:调用哪个工具,或者给出一句回复。中间是 runtime(运行时),负责把这一轮真正跑起来。它调度工具、记下会话和任务状态,该继续就继续,该停就停。最外面是界面,也叫 surface:终端里的交互界面(TUI)、无界面的 exec、编辑器通过 ACP(Agent Client Protocol,智能体客户端协议)连进来,还有 Codex 的 App Server,用来把这套能力开放给其他程序。同一套 runtime 可以挂多个界面。
任务进行到哪、该不该续跑,应该由 runtime 说了算。界面负责显示进度,并把你的输入送进去。你可能会同时开着终端和编辑器,或者把一次无界面的执行接在上次会话后面。界面可以换,任务还在。如果「要不要再跑一轮」是界面在决定,窗口一关,这段判断就没了。

有三件事会贯穿这三层,后面会分开写:权限和沙箱,上下文(包括压缩和记忆),还有验收。Goal 模式也会单写一篇。它要解决的是「一直跑到某个目标完成」。Claude Code、Codex、Grok Build 和 Orca 都有类似的 /goal,完成怎么判定,各家并不一样。
你先要做出的,是中间那一层里最薄的一块:一个能自己转起来的循环。外部工具接口、子代理这些,都先别加。
先跑通最小循环

先只给这五个工具:read_file、glob、grep、bash、edit(或 write)。这已经够它在一个仓库里改代码、跑测试了。换成别的场景,把工具换成那个场景需要的就行。
比如你让它修一个失败的测试。它自己去读相关文件、改代码、跑测试命令,把输出带回来,再决定是继续改还是停。这几步都在循环里完成,不需要你在中间贴代码,再把测试结果复制回去。

Orca 现在的内置工具远不止这些,但核心还是这几个。bash 是唯一的命令入口。你刚开始做的时候,先做成同步执行也行,命令跑完再把输出还给模型。
循环本身很短。下面这段是示意,不是一份完整可运行的仓库。模型名换成你现在能调通的即可。接口按 OpenAI 兼容的 Chat Completions 来就行,DeepSeek 也走这条协议。
messages = [
{"role": "system", "content": "你在本地仓库里改代码。先读再改,改完用命令检查。"},
{"role": "user", "content": task},
]
while True:
resp = client.chat.completions.create(
model=model,
messages=messages,
tools=TOOLS,
)
msg = resp.choices[0].message
messages.append(msg)
if not msg.tool_calls:
print(msg.content)
break
for call in msg.tool_calls:
result = run_tool(call)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result,
})
run_tool 里有两件必须先做的事。
第一,路径不能逃出工作区。相对路径先拼到仓库根上,再解析成绝对路径,出了根目录就拒绝。符号链接也要检查,不然 .. 拦了,链接还能绕出去。

第二,工具结果要诚实。命令失败不能只把标准输出丢回去,装成成功。写入失败更不能回报成功。输出太长要截断,并标明截断了,别把几万行日志原样塞回上下文。
我后来还处理过一种情况:写入全部失败,agent 却返回成功。你先保证每条工具结果里都有明确的成功或失败,已经能避开很多后续的胡改。
循环用什么语言写,选你最快能跑起来的就行。Python 或 TypeScript 都够写最小循环。blade-code 我最初用 TypeScript 写。做 Orca 时切到 Rust,除了沙箱,也看重性能。Rust 的代价也很具体:编译慢,产物体积大,占磁盘,会影响开发迭代。等你真的要做系统沙箱,或者性能成了瓶颈,再考虑换语言。不必因为 Orca 是 Rust 写的,一上来就用 Rust。
这个系列怎么读
一共 19 篇。前半先把能跑起来的骨架搭上。中间几组把现在必须单独说清的问题写清楚:runtime 归谁管、Goal 怎么判断做完、上下文怎么压、记忆怎么留、沙箱和验收怎么做。后半才是扩展和对外协议。用到哪一层,再看对应的那几篇就行。
开篇。第(一)篇就是这篇,先看清层,再跑通最小循环。第(二)篇写 harness 是什么:运行时本身、长任务时怎么把进度留下,以及仓库里给 agent 用的规范。
骨架。第(三)篇是入口、配置与运行模式:一个二进制怎么分到终端、无界面执行和编辑器接入,哪些参数其实是安全边界。第(四)篇谈模型和 Provider,包括流式输出、前缀缓存,以及请求可以无状态、会话必须有状态。第(五)篇把 runtime 和事件流展开,说明任务进行到哪为什么要由 runtime 来管。
指令和循环。第(六)篇是 Prompt 和指令层,包括系统提示、AGENTS.md 这类项目指令,以及技能说明怎么按需展开。第(七)篇写 Agentic Loop:让模型自己决定、设上限、流式输出。Goal 是循环外面的一层,放到第(八)篇,对照几家怎么判断该停、怎么续跑。第(九)篇写长任务、中断与恢复。
上下文。第(十)篇谈会话怎么存、恢复时读哪份记录。第(十一)篇是上下文压缩:什么时候压、压掉什么,压完后关键约束怎么补回去。第(十二)篇写 Memory:你主动记住的,和自动留下的跨会话记忆。
工具、权限、验收。第(十三)篇讲工具怎么建模和注册,也会对照「默认只放几个工具」和「做成大工具箱再靠权限约束」这两种做法。第(十四)篇是执行管线和权限模式,谁可以跑,分哪些档。第(十五)篇写沙箱:批准之后还能碰到哪,以及我为什么从 TypeScript 换到 Rust。第(十六)篇谈验收,别只听它说做完了。
生态和对外。第(十七)篇写 Skills、Plugins、Hooks 和 Slash Commands,循环稳定之后再加。第(十八)篇是子 Agent 与多 Agent:什么时候该把脏活分出去。Pi 故意不做子代理,这是一种设计选择。第(十九)篇写对外接口和界面:MCP、ACP、App Server,以及无界面执行和后台进程。
先做到这几条
把最小循环跑通,可以按下面几条验收:
- 五个基本工具能跑,循环能自己转完一个小任务,比如改一个函数并跑通相关测试
- 路径出不了工作区,符号链接也绕不出去
- 工具失败会明确报失败,长输出会被截断,并标明截断了
- 循环能在模型不再调工具时停下来,你也能在中途拦住它
这几条做实了,再往下补权限档位、上下文压缩和验收。语言先选你熟的。
这套模型、工具、循环和运行时约束,换掉工具和验收方式,就能用到写代码以外的场景。Claude 的 Cowork 用来整理文件、做调研、出文档和表格。OpenAI 的 Codex 也有写简报、做汇报、做摘要这类用法。
继续阅读
讨论
读到这里时加载讨论。