侧边栏壁纸
博主头像
写码小Cの秘密基地

世界上只有一种真正的英雄主义,那就是在认清生活的真相之后,依然热爱生活。

  • 累计撰写 2 篇文章
  • 累计创建 4 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

基于 Eino 框架从 0 到 1 构建一个 Coding Agent

写码小C
2026-07-03 / 1 评论 / 0 点赞 / 18 阅读 / 0 字

基于 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_filesread_filewrite_filerun_command
  • write_filerun_command 做人工审批。
  • 使用 Eino ADK 的 interrupt/resume 机制完成“中断等待审批,然后继续执行”。

这个范围足够小,但已经覆盖了 Coding Agent 最核心的工作流:理解项目、修改文件、运行验证、继续对话。

项目结构

Ageino 的代码结构刻意保持简单:

模块职责
cmd/ageino/main.goCLI 入口,支持 chatrun 两种模式
internal/config从环境变量读取模型、workspace、超时等配置
internal/sessionChatSession,维护多轮历史和 ADK Runner
internal/tools注册和实现 list_filesread_filewrite_filerun_command
internal/approvalApprovableTool,给高风险工具叠加审批中断层
internal/events消费 AgentEvent,打印 token、工具调用和中断信息
internal/checkpoint内存 checkpoint,用于 interrupt/resume

这里有一个设计取舍:Ageino 没有自己手写 Agent 主循环,而是把主循环交给 Eino ADK。我们自己实现的是外围工程能力,比如 CLI、工具、安全边界、审批、事件打印和会话历史。

核心链路:一轮对话是怎么跑起来的

用户在 CLI 中输入一条指令后,Ageino 会调用 ChatSession.RunTurn()。这一轮大致分成五步:

用户输入
追加到 s.history
runner.Run 传入历史和 checkpointID
消费 ADK AgentEvent 流
是否遇到工具审批中断
收集 assistant 最终回复
等待用户 y/N 审批
ResumeWithParams 恢复执行
追加 AssistantMessage 到历史

s.history 是 Ageino 自己维护的内存会话历史。它只保存用户消息和 assistant 的最终文本回复,不保存中间工具调用细节。这样实现简单,便于理解“多轮会话记忆”的最小形态。

真正的 Agent 执行由 Eino ADK 的 Runner 负责。每次调用 runner.Run() 时,ADK 会基于当前消息列表驱动 ChatModelAgent,模型根据 system prompt 和 tools schema 决定是否调用工具。如果模型调用工具,ADK 会执行对应的 tool;如果工具触发中断,事件流里就会出现 interrupted event。

Agent循环架构详图

SVG高清架构图,可放大阅览细节。

core-loop-architecture.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_filesread_file。高风险工具包括 write_filerun_command,它们会先包一层 approval.ApprovableTool,真正执行前必须经过人工审批。

这个设计比较贴近真实 Coding Agent 的权限模型:读项目通常可以直接放行,但写文件和执行命令必须有明确边界。

审批机制:ApprovableTool 与 interrupt/resume

Ageino 里最值得单独理解的是 ApprovableTool。它不是重新实现一个工具,而是包装已有的 einotool.InvokableTool

type ApprovableTool struct {
    einotool.InvokableTool
}

这意味着原始工具的 Info()、schema、执行逻辑都还在,ApprovableTool 只是在 InvokableRun() 外层加了一道审批逻辑。

第一次调用高风险工具时,ApprovableTool 不会直接执行,而是调用 Eino 的 StatefulInterrupt

CLI ApproverEino ADK RunnerApprovableToolLLMCLI ApproverEino ADK RunnerApprovableToolLLM请求执行 write_file 或 run_command第一次进入 InvokableRunStatefulInterrupt 保存工具名和原始参数产出 interrupted event展示审批信息并等待 y/N

用户审批后,ChatSession.consume() 会调用 runner.ResumeWithParams(),把审批结果带回 ADK。随后 ApprovableTool.InvokableRun() 第二次进入,此时可以通过 GetInterruptState 取回第一次保存的原始参数,再通过 GetResumeContext 拿到审批结果。

同意
拒绝
用户审批结果
是否同意
ResumeWithParams 携带 Result
GetInterruptState 取回原始参数
GetResumeContext 读取审批结果
执行原始工具 InvokableRun
工具结果回到模型上下文
返回 tool denied
模型基于拒绝结果继续响应

这里有一个关键点: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 这条主线吃透,后续扩展才不会变成堆功能。

后续可以怎么演进

下一步我会优先考虑几个方向:

  1. 增加更细粒度的文件编辑工具,比如 edit_file,避免每次用 write_file 覆盖整文件。
  2. 改进命令执行层,明确 PowerShell、Git Bash、sh 等不同 shell 的语法差异。
  3. 在审批前展示更友好的 diff 或命令预览。
  4. 增加会话持久化,把用户消息、assistant 回复、工具调用和工具结果都序列化。
  5. 把 Eino compose 的 graph/workflow 能力作为子流程或 GraphTool 接入,而不是替代 ADK 主循环。

其中我最看重的是第五点。通过这个项目我更明确地理解了 Eino 的两层能力:compose 更适合确定性流程编排,ADK 更适合 Agent 运行时编排。Coding Agent 的主循环应该交给 ADK,而一些结构化、可复用的子任务可以用 compose 封装成工具接入。

总结

Ageino 这个项目的价值不在于功能多,而在于把一个 Coding Agent 的最小骨架跑通了:

只读工具
高风险工具
CLI 输入
ChatSession 多轮历史
Eino ADK Runner
ChatModelAgent
是否调用工具
list_files / read_file
write_file / run_command 触发 interrupt
用户审批
resume 恢复执行
工具结果
assistant 回复

对我来说,这个过程最大的收获是:Eino ADK 不是简单的“模型调用封装”,它真正接管了 Agent 运行时里最容易写散的部分,包括工具循环、事件流、中断恢复和 checkpoint。而我们要做的,是围绕它补齐工程边界:工具、安全、会话、交互和可观测性。

从这个角度看,基于 Eino 从 0 到 1 构建 Coding Agent,并不是从零手写一个框架,而是学会如何把框架提供的 Agent 能力落到一个可运行、可理解、可演进的小项目里。

0

评论区