21|Agent 与 Graph 结合:自主决策,确定性执行
《Eino 实战:用 Go 构建 AI 应用与智能体》· 第 21 篇
关键词:GraphTool、AgentAsTool、流程复用、职责边界、确定性
前置条件:已完成 16|从流程到 Agent 与 11|Tool 与 ToolsNode。
前几篇我们分别用 Graph 编排流程、用 Agent 做自主决策。现实项目里两者往往都要:流程需要稳定可预测,但入口又想灵活。
直觉做法是把 Agent 塞进 Graph 当节点。第一篇引用的官方辨析明确指出,这样做不优雅——Agent 需要多轮交互、长时记忆和异步输出,而 Graph 节点期待确定性的输入输出。
正确的结合方向是反过来的:把 Graph 封装成 Agent 的工具。
二、为什么不是“Agent 作为节点”
先把“反向方案”为什么不合适说清楚,这有助于理解边界。
| 特征 | Graph 节点 | Agent |
|---|---|---|
| 输入 | 上游节点的输出 | 结构化输入加上会话历史 |
| 输出 | 确定的结构化值 | 异步的全过程事件 |
| 执行 | 同步、可预期 | 多轮、路径不定 |
把 Agent 当作 Graph 节点,会同时踩三个坑:
- 输入不完整。Agent 依赖历史,而 Graph 节点只拿到上游输出。
- 输出不好用。后续节点拿到的是一串事件,而不是一个确定的值。
- 阻塞主流程。Agent 的多轮交互会卡住本应快速推进的编排。
官方文档的结论很直接:Graph 中需要的是一个纯粹的 LLM 节点(做好意图识别或内容生成),而不是完整的 Agent。如果确实需要 Agent 的自主性,更好的做法是给 Agent 加前置/后置处理,而不是把它塞进静态图里。
三、Graph 与 Tool 的相似性
反过来看,为什么“Graph 作为工具”就自然得多?
| 维度 | Graph | Tool |
|---|---|---|
| 输入 | 结构化数据 | 结构化数据(JSON 参数) |
| 交付物 | 最终结果 | 最终结果 |
| 状态 | 单次执行 | 单次执行 |
| 同步性 | 同步 | 对模型而言是同步 |
两者几乎是同一个形状。因此封装起来几乎没有阻抗:Graph 负责确定性的执行,Tool 接口负责与 Agent 对接。
网上很流行的一张关系图概括了这一点:
Agent:理解目标、决定何时行动 └─ 调用 Tool └─ Tool 内部封装 Graph └─ Graph 按预定步骤确定执行 └─ 返回确定性结果给 Agent四、完整示例
下面这段程序把一个知识问答流程封装成工具,不访问外部服务:
packagemainimport("context""encoding/json""fmt""log""strings""github.com/cloudwego/eino/components/tool""github.com/cloudwego/eino/compose""github.com/cloudwego/eino/schema")// ---- 一个确定性的知识问答流程:先整理资料,再生成回答 ----funcbuildKnowledgeGraph()(compose.Runnable[string,string],error){g:=compose.NewGraph[string,string]()// 节点 1:检索(真实项目接 Retriever,这里用固定资料模拟)。retrieve:=compose.InvokableLambda(func(_context.Context,_string)(string,error){return"资料:打印机离线时,先检查网线,再确认网段。",nil})// 节点 2:生成回答(真实项目接 ChatModel,这里用字符串处理模拟)。answer:=compose.InvokableLambda(func(_context.Context,ctxTextstring)(string,error){return"根据资料,"+strings.TrimPrefix(ctxText,"资料:"),nil})iferr:=g.AddLambdaNode("retrieve",retrieve);err!=nil{returnnil,err}iferr:=g.AddLambdaNode("answer",answer);err!=nil{returnnil,err}_=g.AddEdge(compose.START,"retrieve")_=g.AddEdge("retrieve","answer")_=g.AddEdge("answer",compose.END)returng.Compile(context.Background())}// ---- 把图封装为工具 ----typegraphToolstruct{namestringdescstringrun compose.Runnable[string,string]}func(t*graphTool)Info(_context.Context)(*schema.ToolInfo,error){return&schema.ToolInfo{Name:t.name,Desc:t.desc,ParamsOneOf:schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{"query":{Type:schema.String,Desc:"用户问题"},}),},nil}func(t*graphTool)InvokableRun(ctx context.Context,argsJSONstring,_...tool.Option)(string,error){varargsstruct{Querystring`json:"query"`}iferr:=json.Unmarshal([]byte(argsJSON),&args);err!=nil{return"",fmt.Errorf("参数解析失败: %w",err)}// 图在工具内部以确定性方式执行,输入输出都是普通值。returnt.run.Invoke(ctx,args.Query)}funcmain(){iferr:=run();err!=nil{log.Fatal(err)}}funcrun()error{ctx:=context.Background()graph,err:=buildKnowledgeGraph()iferr!=nil{returnfmt.Errorf("构建图失败: %w",err)}gt:=&graphTool{name:"knowledge_qa",desc:"基于内部知识库回答问题",run:graph}// 编译期断言:这个类型满足 Eino 的工具接口,可直接交给 Agent。var_tool.InvokableTool=gt info,err:=gt.Info(ctx)iferr!=nil{returnerr}fmt.Printf("工具名=%s 描述=%s\n",info.Name,info.Desc)out,err:=gt.InvokableRun(ctx,`{"query":"打印机离线怎么办"}`)iferr!=nil{returnerr}fmt.Println("工具输出:",out)returnnil}实机输出:
工具名=knowledge_qa 描述=基于内部知识库回答问题 工具输出: 根据资料,打印机离线时,先检查网线,再确认网段。关键点在于var _ tool.InvokableTool = gt这行编译期断言:只要类型满足接口,它就能像第 11 篇的任何工具一样交给ToolsNode或ChatModelAgent。对 Agent 而言,这个工具背后是图还是函数,它并不关心。
五、官方封装方式
上面手写的graphTool有教学价值,但每个项目重复写一遍并不划算。Eino 官方在示例仓库中提供了graphtool包,思路正是一样的:把任意 Graph 转成InvokableTool。
// 官方 graphtool 的签名(来自 eino-examples)funcNewInvokableGraphTool[I,O any](graph compose.Graph[I,O],name,descstring,opts...compose.GraphCompileOption,)(*InvokableGraphTool[I,O],error)它内部处理了这几件事:
- 由输入类型推导工具的 JSON Schema。
- 把模型给的 JSON 参数反序列化成
I。 - 编译并执行图。
- 处理中断(图内也可能有 HITL 点)。
- 把输出
O序列化成字符串返回。
生产项目建议直接用官方实现,而不是自己造一遍。参考:
https://github.com/cloudwego/eino-examples/tree/main/adk/common/tool/graphtool官方还提供了三个典型示例:Chain 文档摘要、Graph 多源研究、Workflow 订单处理——覆盖了三种编排方式封成工具的场景。
六、这套组合的价值在哪
| 好处 | 说明 |
|---|---|
| 复用现有流程 | 已经调好的 RAG 流程、数据处理管道,不必为 Agent 重写 |
| 保证关键步骤确定 | 授权、计算、落库等步骤在图内按固定顺序执行 |
| 缩小暴露面 | Agent 只看到一个工具,而不是内部十几个节点 |
| 便于测试 | 图可以脱离 Agent 单独测试,各测各的 |
一句话概括:让 Agent 决定“做什么”,让 Graph 决定“怎么做”。
这个分工也回应了第一篇的一句话:模型可以提出行动,但关键业务怎么执行,应该由可信代码控制。
七、边界与注意事项
1. 图内不要依赖对话历史。
图的输入是结构化参数。需要历史信息时,应由 Agent 在调用工具时把相关内容整理进参数,而不是让图自己去猜。
2. 输入输出要保持可序列化。
工具的入参是 JSON 字符串,出参也必须是字符串。复杂的结构要设计好序列化格式,并考虑长度限制。
3. 图内的中断需要透传。
如果图内含审批点(第 20 篇),封装时要确保中断能正确传播到外层,否则 Agent 会“卡住”而用户看不到确认提示。官方graphtool已处理这一点,自己实现时务必注意。
4. 工具描述决定 Agent 会不会选它。
和所有工具一样,描述要写清使用时机。图内部再精巧,描述含糊也会被模型忽略。
5. 鉴权仍在最内层。
图封装成工具后,权限校验依然要在图内部或具体工具内完成。“藏在图里”不是安全措施。
八、什么时候该拆、什么时候该合
| 情况 | 建议 |
|---|---|
| 流程固定、需要复用 | 封装为工具,交给 Agent 调用 |
| 流程需要 Agent 逐步交互 | 不要把 Agent 塞进图;改为给 Agent 加处理逻辑 |
| 只是想做一次意图识别或生成 | 用 LLM 节点,不需要完整 Agent |
| 关键步骤必须按顺序且可审计 | 放进图内,作为工具暴露 |
判断标准是**“这一步是否需要反复交互”**。需要交互的交给 Agent,一次完成且结果确定的封装成工具。
九、小结
- 把 Agent 当作 Graph 节点会造成输入不完整、输出不好用、阻塞主流程三个问题。
- Graph 与 Tool 在输入、交付物、状态、同步性上高度相似,天然适合互相封装。
- 用
Info+InvokableRun就能把图变成工具;官方graphtool包已提供成熟实现。 - Agent 决定做什么,Graph 决定怎么做。
- 图内不要依赖对话历史;注意序列化、长度限制和中断透传。
- 鉴权仍须在最内层完成,封装不构成安全边界。
第四篇《Agent 开发》到此完成。下一篇进入第五篇,讨论多智能体协作。
参考资料与版本说明
资料核对日期为2026 年 10 月 9 日:
- Agent 还是 Graph?AI 应用路线辨析
- Eino 官方示例:graphtool
- Eino 主仓库
本文示例按Eino v0.9.21编写,图内节点使用自定义 Lambda(不访问外部服务),已实机运行,正文输出为真实结果。官方graphtool包的签名与职责依据示例仓库文档整理,未在本篇实机运行。