基于 Eino 框架从 0 到 1 构建一个 Coding Agent
最近我在系统学习 CloudWeGo Eino。前面把模型调用、工具调用、compose 编排、ADK Agent 等核心层次基本过了一遍之后,我想做一个更贴近真实场景的小产出:不用手写一套完整 Agent 框架,而是基于 Eino ADK,从 0 到 1 实现一个最小可用的 Coding Agent。
这个项目我叫它 Ageino(Agent + Eino)。它不是要复刻 Claude Code、Cursor Agent 或 DeepSeek-Reasonix 的全部能力,而是先抓住 Coding Agent 的最小闭环:用户可以和 Agent 多轮对话,Agent 能读取文件、写文件、执行命令,并且在高风险操作前触发人工审批。只要这个闭环跑通,后续再扩展 session 持久化、MCP、子 Agent、skills、桌面端 UI,都会更有依据。
为什么先做一个最小 Agent
如果一开始就追求“完整 Agent”,很容易被功能列表带偏:会话管理、权限系统、MCP Server、前端界面、插件体系、任务队列、子 Agent 调度,每一个方向都可以单独展开很久。但对学习 Eino 来说,最重要的不是堆功能,而是理解 Eino 在 Agent 项目里到底承担哪一层职责。
我最后把 MVP 边界收缩成几个核心点:
- 支持 CLI 交互,先不做桌面端和 Web UI。
- 支持同一进程内的多轮会话记忆。
- 使用 Eino ADK 的
ChatModelAgent承担 ReAct 主循环。 - 提供最基础的 coding tools:
list_files、read_file、write_file、run_command。 - 对
write_file和run_command做人工审批。 - 使用 Eino ADK 的 interrupt/resume 机制完成“中断等待审批,然后继续执行”。
这个范围足够小,但已经覆盖了 Coding Agent 最核心的工作流:理解项目、修改文件、运行验证、继续对话。
项目结构
Ageino 的代码结构刻意保持简单:
| 模块 | 职责 |
|---|---|
cmd/ageino/main.go | CLI 入口,支持 chat 和 run 两种模式 |
internal/config | 从环境变量读取模型、workspace、超时等配置 |
internal/session | ChatSession,维护多轮历史和 ADK Runner |
internal/tools | 注册和实现 list_files、read_file、write_file、run_command |
internal/approval | ApprovableTool,给高风险工具叠加审批中断层 |
internal/events | 消费 AgentEvent,打印 token、工具调用和中断信息 |
internal/checkpoint | 内存 checkpoint,用于 interrupt/resume |
这里有一个设计取舍:Ageino 没有自己手写 Agent 主循环,而是把主循环交给 Eino ADK。我们自己实现的是外围工程能力,比如 CLI、工具、安全边界、审批、事件打印和会话历史。
核心链路:一轮对话是怎么跑起来的
用户在 CLI 中输入一条指令后,Ageino 会调用 ChatSession.RunTurn()。这一轮大致分成五步:
s.history 是 Ageino 自己维护的内存会话历史。它只保存用户消息和 assistant 的最终文本回复,不保存中间工具调用细节。这样实现简单,便于理解“多轮会话记忆”的最小形态。
真正的 Agent 执行由 Eino ADK 的 Runner 负责。每次调用 runner.Run() 时,ADK 会基于当前消息列表驱动 ChatModelAgent,模型根据 system prompt 和 tools schema 决定是否调用工具。如果模型调用工具,ADK 会执行对应的 tool;如果工具触发中断,事件流里就会出现 interrupted event。
Agent循环架构详图
SVG高清架构图,可放大阅览细节。
为什么选择 ADK,而不是手写 ReAct loop
传统手写 Agent 往往要自己处理这几件事:
- 拼接 system prompt、历史消息和工具结果。
- 调用模型并解析 tool call。
- 执行工具,把 tool result 再塞回模型上下文。
- 判断是否继续循环。
- 处理中断、恢复、事件流和 checkpoint。
这些逻辑并不难写出第一版,但要写稳并不简单。Eino ADK 的价值就在这里:它把 Agent 执行抽象成 Agent + Runner + AgentEvent,开发者可以把注意力放在“Agent 能力边界”上,而不是每个项目都重复造一套 ReAct 调度器。
在 Ageino 里,ADK 的角色很明确:
| ADK 组件 | 在 Ageino 中的职责 |
|---|---|
ChatModelAgent | 负责模型驱动的 ReAct 决策 |
Runner | 负责执行 Agent 并产出事件流 |
AgentEvent | 向外暴露 token、tool call、tool result、interrupt 等运行过程 |
Checkpoint | 在中断恢复时保存上下文 |
所以 Ageino 的核心不是“重新发明 Agent”,而是学习如何把 Eino ADK 接进一个真实 Coding Agent 的工程外壳里。
工具系统:从 Go 函数到模型可调用工具
Ageino 目前只实现了四个工具:
| 工具 | 能力 | 是否需要审批 |
|---|---|---|
list_files | 列出 workspace 内目录内容 | 否 |
read_file | 读取 workspace 内文本文件 | 否 |
write_file | 写入 workspace 内文件 | 是 |
run_command | 在 workspace 内执行 shell 命令 | 是 |
这些工具本质上都是普通 Go 方法。通过 Eino 的 utils.InferTool,可以根据函数签名和结构体 tag 自动生成 tool schema,把 Go 函数变成模型可以调用的 InvokableTool。
注册工具时做了一个简单分层:
只读工具直接注册,包括 list_files 和 read_file。高风险工具包括 write_file 和 run_command,它们会先包一层 approval.ApprovableTool,真正执行前必须经过人工审批。
这个设计比较贴近真实 Coding Agent 的权限模型:读项目通常可以直接放行,但写文件和执行命令必须有明确边界。
审批机制:ApprovableTool 与 interrupt/resume
Ageino 里最值得单独理解的是 ApprovableTool。它不是重新实现一个工具,而是包装已有的 einotool.InvokableTool:
type ApprovableTool struct {
einotool.InvokableTool
}
这意味着原始工具的 Info()、schema、执行逻辑都还在,ApprovableTool 只是在 InvokableRun() 外层加了一道审批逻辑。
第一次调用高风险工具时,ApprovableTool 不会直接执行,而是调用 Eino 的 StatefulInterrupt:
用户审批后,ChatSession.consume() 会调用 runner.ResumeWithParams(),把审批结果带回 ADK。随后 ApprovableTool.InvokableRun() 第二次进入,此时可以通过 GetInterruptState 取回第一次保存的原始参数,再通过 GetResumeContext 拿到审批结果。
这里有一个关键点:StatefulInterrupt 保存了原始工具参数。恢复执行时,不需要让模型重新生成参数,也不需要自己在外部保存一份临时状态。这个机制让“工具调用前审批”变得非常自然。
workspace 沙箱:最小但必要的安全边界
Coding Agent 最大的风险不是模型会说错话,而是模型真的能改文件、执行命令。所以 Ageino 从一开始就加了 workspace 限制。
所有文件工具都会先调用 Workspace.Resolve()。它会把模型传入的路径解析成绝对路径,再用 filepath.Rel 判断目标是否仍然位于 workspace 内。如果模型尝试访问 ../ 之外的路径,工具会直接拒绝。
命令执行也固定在 workspace 目录下:
cmd.Dir = t.workspace.Root()
同时 run_command 还有一个基础命令守卫,用来拦截明显危险的命令。这个守卫不是完整权限系统,但对 MVP 来说,它先建立了一个重要原则:Agent 的能力必须被工程边界约束,不能完全依赖 prompt 约束。
多轮记忆:先做内存态,不急着持久化
Ageino 的多轮记忆非常朴素:ChatSession 内部有一个 history []adk.Message。每轮用户输入追加 UserMessage,每轮模型最终回复追加 AssistantMessage。
这和更完整的 Agent 项目有明显区别。比如 DeepSeek-Reasonix 这类项目通常会持久化完整会话,包括工具调用、工具结果、思考过程、任务状态等。那种设计更适合断线恢复、跨进程继续执行和复杂 UI 展示。
但 Ageino 的目标是学习 Eino ADK 的核心机制,所以第一版只保留内存态历史。这能让代码保持清晰,也能把注意力集中在 Agent 执行链路本身。
CLI 为什么足够了
一开始我也考虑过 TUI 或 Web UI,但最后选择了最简单的 CLI:
go run ./cmd/ageino chat --workspace .
原因很简单:这个阶段核心是后端 Agent 机制,不是交互界面。CLI 已经能覆盖多轮对话、流式输出、工具调用展示、审批输入这些关键路径。等后端稳定后,再做 Web、TUI 或桌面端,只是把输入输出层换掉,不应该影响核心架构。
这版 MVP 没做什么
Ageino 当前刻意没有实现这些能力:
- 没有 session 持久化。
- 没有 MCP。
- 没有 skills。
- 没有子 Agent。
- 没有 LSP。
- 没有复杂权限策略。
- 没有桌面端或 Web UI。
这些不是不重要,而是不适合第一阶段一起做。先把 ChatModelAgent + Runner + Tools + Interrupt/Resume + Memory 这条主线吃透,后续扩展才不会变成堆功能。
后续可以怎么演进
下一步我会优先考虑几个方向:
- 增加更细粒度的文件编辑工具,比如
edit_file,避免每次用write_file覆盖整文件。 - 改进命令执行层,明确 PowerShell、Git Bash、sh 等不同 shell 的语法差异。
- 在审批前展示更友好的 diff 或命令预览。
- 增加会话持久化,把用户消息、assistant 回复、工具调用和工具结果都序列化。
- 把 Eino compose 的 graph/workflow 能力作为子流程或 GraphTool 接入,而不是替代 ADK 主循环。
其中我最看重的是第五点。通过这个项目我更明确地理解了 Eino 的两层能力:compose 更适合确定性流程编排,ADK 更适合 Agent 运行时编排。Coding Agent 的主循环应该交给 ADK,而一些结构化、可复用的子任务可以用 compose 封装成工具接入。
总结
Ageino 这个项目的价值不在于功能多,而在于把一个 Coding Agent 的最小骨架跑通了:
对我来说,这个过程最大的收获是:Eino ADK 不是简单的“模型调用封装”,它真正接管了 Agent 运行时里最容易写散的部分,包括工具循环、事件流、中断恢复和 checkpoint。而我们要做的,是围绕它补齐工程边界:工具、安全、会话、交互和可观测性。
从这个角度看,基于 Eino 从 0 到 1 构建 Coding Agent,并不是从零手写一个框架,而是学会如何把框架提供的 Agent 能力落到一个可运行、可理解、可演进的小项目里。
评论区