跳到正文

Coding Agent 原理与实践

00:为什么开发 Ink

更新于:

作者:折腾客

约 2,386 字

文章目录

向 Coding Agent 提出“修好这个问题”的要求后,它就可能开始找文件、改代码、跑测试。模型回复之后,是谁在安排下一步?

AI 模型的能力越来越强,围绕模型搭建的工具也让不少工作变得更高效。我开始参考 pi,从零开发 Ink。pi 是一个支持自定义和扩展的终端编程助手。

这个系列就拿 Ink 的实现来拆解 Coding Agent。开篇先聊为什么要做、需要哪些模块,以及后面准备讲什么。

1. 为什么想自己做一个 Coding Agent

我用过一些现成的编程助手。它们能做很多事,但用下来,总有些地方不太合自己的习惯。

对我这样的国内用户来说,Claude Code 的使用门槛让我觉得不够友好。用 Codex 时,我又希望看到更多推理过程,产品提供的展示没有完全满足我的需求。我接触到的一些国产工具,有的设计大量沿用一线产品,有的实现还比较简陋,暂时没找到特别合意的。

这些不顺手的地方,让我开始关注 Coding Agent 中的 harness 层。它负责把模型、工具、上下文和任务状态组织起来:给模型提供需要的信息,执行模型提出的工具调用,再把执行结果交给模型,让任务能够继续进行。模型与这层软件配合,构成我们使用的 Coding Agent(下文简称 Agent)。

我想通过开发 Ink,理解这些模块的实现,也做一个更符合自己使用习惯的编程助手。这个系列会逐个介绍这些模块的原理和实现方式。

对于模型的推理内容,Agent 能展示多少,取决于模型服务返回了什么。工具调用、执行结果和发送给模型的上下文,则可以由 harness 记录,再通过交互层展示给用户。

2. 为什么选择 pi 作为参考

pi 吸引我的地方,是核心设计简洁,也方便按自己的需要增加功能。我想先理解一个编程助手的基本实现,再考虑加入会话、交互和扩展等功能。

做 Ink 时,我先让模型能调用工具、拿到结果。之后每增加一部分,都先想清楚它要解决什么问题。

这里说的“从零开发”,指自己实现运行逻辑、工具和状态管理,并不意味着所有东西都重写。比如我用 TypeScript 和 Node.js 开发 Ink,终端 UI 复用了 pi 的组件库。讲到相关章节时,会交代哪些部分自己实现,哪些部分复用了现成组件。

3. 一个任务是怎样跑起来的

假设项目中的求和函数执行 sum(2, 3) 时返回了 23,预期结果是 5。我们交给 Agent 这样一个任务:“找到求和结果错误的原因,修复函数,并运行相关测试。”

在这个例子中,我们还没有告诉模型函数写在哪,也没有提供测试结果。下面的流程图展示一次请求怎样变成多次模型调用:有工具请求,就执行并带着结果继续;没有工具请求,就显示回答并结束。本图只说明基本流程,并非真实运行记录。

基本流程示意。工具结果加入历史后,沿右侧箭头回到“调用模型”;没有工具请求时,沿左侧箭头结束运行。

用户中断、达到轮数上限或模型请求失败也可能结束运行,01 章再展开这些情况。运行结束后,仍需检查修改和测试结果,判断任务是否完成。

比如模型请求读取文件,读取工具负责打开文件并返回内容;执行循环把这个结果放进消息历史,再次调用模型。接着,模型可能要求修改函数、运行测试。测试失败时,错误输出也会加入历史,供它继续判断。

一次 Agent 运行往往包含多次模型调用。Agent Loop(执行循环) 负责串起这些步骤:调用模型、执行工具、记录结果,再判断是否继续。01 章会用更小的“读取 README”场景,逐步介绍执行循环的实现。

4. 开发一个 Coding Agent,需要哪些模块

沿着刚才的任务,可以把 harness 层的主要工作分成下面几部分。模型通过模型接入层与这些模块配合。这张图按职责分组,源码里的一个文件或类不一定对应一个方框。

用户提出任务目标、约束与运行中的补充
↓ 输入任务 ↑ 显示进度与回答

Coding Agent 的 harness 层

交互与运行控制接收输入、显示进度、处理停止与追加要求
↓ 启动与控制 ↑ 进度与结果
模型接入转换不同模型服务的消息格式转换文本、图片等输入
Agent LoopAgent 循环请求模型 → 执行工具记录结果,再决定是否继续
工具系统读取、编辑、搜索、命令也可接入 MCP 服务工具

Agent Loop 通过模型接入层获取回复,调用工具,并把执行结果交回模型。

配套模块 参与运行的不同阶段

上下文管理
选择发给模型的信息,按需加载 Skills,压缩过长历史。
会话管理
保存消息历史,支持恢复任务与创建分支。
权限与信任
决定是否加载项目资源;通过扩展接入工具操作审批。
扩展机制
增加工具、命令,或在运行过程中执行自定义逻辑。

子代理协作主 Agent 委派子任务,其他 Agent 单独处理并返回结果。

每个实线框表示一个模块,最外层框表示 harness 层的范围。箭头表示调用与反馈;配套模块不按图中排列顺序执行。测试与评估覆盖所有模块。

4.1 模型接入、执行循环与工具

模型接入负责向模型服务发请求,把返回的数据转换成 执行循环使用的统一消息格式。不同服务的消息格式、工具调用和流式响应可能不同,需要在这里处理。

执行循环决定拿到回复后做什么:有工具请求就执行,把结果加入历史后再次调用模型;没有工具请求,或达到停止条件,就结束本次 Agent 运行。

工具系统负责实际操作文件和运行命令。模型发来一个“读取文件”的请求,打开文件的是工具代码;它还要处理路径、参数和读取失败的情况。

把这三部分接起来,再接入模型服务,就能运行一个最小 Agent。之后还可以增加输入和工具的种类:图片输入需要把图片带进上下文,再由模型接入层转换为服务支持的格式;MCP 则是一种连接外部服务的协议,Ink 可以把服务提供的工具接入现有工具系统。

4.2 上下文与会话

任务一长,消息历史就会不断增长。上下文管理要决定每次给模型发送哪些信息,放不下时删减或压缩什么。摘要可能丢失细节,不能只看它省了多少 token。

有些工作说明只在特定任务中需要,例如代码审查步骤。Skills 将这些说明和相关资料组织起来,Ink 先向模型提供目录,再按需读取正文,避免一开始就把所有内容放进上下文。

如果关闭 Agent 后,下次启动时还想继续任务,或者想从某条消息另开一个分支,就需要会话管理。它负责保存和恢复历史;上下文管理则决定本次请求要把哪些信息发给模型。

4.3 交互、权限、扩展与协作

交互层负责接收输入、显示执行进度、处理停止和追加要求。用量统计则汇总模型调用消耗的 Token,帮助用户了解任务开销;累计用量与当前上下文占用是两个不同的数值。

项目信任检查决定是否加载特定项目资源。工具执行前的权限检查则通过扩展接入,例如加载并配置 Guardian 后,可以按规则阻止操作或请求用户批准。扩展机制还允许用户增加工具、命令和其他自定义行为。

通过子代理机制,主 Agent 可以把子任务交给其他 Agent 单独处理,再收回结果。如果多个 Agent 同时工作,还需要处理并发协作:各自能看到什么上下文,结果怎样返回,同时修改文件会不会冲突。

测试与评估会贯穿这些模块。工具按预期执行了,并不代表 Agent 完成了用户的任务;除了检查代码逻辑,也要用实际任务验证效果。

5. 系列目录与阅读方式

这个系列面向用过 Coding Agent、想理解内部原理的读者。我们先理解执行循环,再看模型接入和工具怎样实现,之后讨论长任务、交互和扩展。

第 10 章分为上、中、下三篇,依次讨论子代理委派、并发协作和续跑。第 11—15 篇将分别展开 Skills、MCP、图片输入、用量统计和模型管理。目录中标为“计划”的文章尚未发布。

00 是导读,后面的模块章节以场景、源码和异常处理为主线,按需加入模拟运行:

  1. 一个小场景。 先看用户想做什么,这个模块在什么时候起作用。
  2. 源码与原理。 从 Ink 中截取关键代码,保留函数和变量名,加上必要的注释。简化过的地方会注明,方便对照源码。
  3. 模拟运行(按需)。 当状态变化、执行时序或数据累积不容易从源码中看清时,用逐步演示辅助理解;若只是重复已有流程,则省略。演示中的模拟数据会明确标注。
  4. 异常处理。 看哪些地方会出错,对应模块如何处理错误,以及处理后还要保留什么信息。

实现里的取舍会放在对应问题旁边解释。想继续看代码时,可以顺着文章提供的源码链接阅读完整实现。

6. 从最小 Agent 开始

下一篇从一个小任务开始:“读取 README.md,告诉我项目怎样启动。”

先把模型调用和工具执行当作两个已有函数,看看如何把它们接成一个循环,再处理工具失败、轮数上限和用户中断。

文章会收录在专题页。Ink 的源码和项目说明放在 GitHub 仓库,具体功能与使用方式以仓库文档为准。