ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

21|Agent 与 Graph 结合:自主决策,确定性执行 - eino教程

21|Agent 与 Graph 结合:自主决策,确定性执行 - eino教程

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 节点,会同时踩三个坑:

  1. 输入不完整。Agent 依赖历史,而 Graph 节点只拿到上游输出。
  2. 输出不好用。后续节点拿到的是一串事件,而不是一个确定的值。
  3. 阻塞主流程。Agent 的多轮交互会卡住本应快速推进的编排。

官方文档的结论很直接:Graph 中需要的是一个纯粹的 LLM 节点(做好意图识别或内容生成),而不是完整的 Agent。如果确实需要 Agent 的自主性,更好的做法是给 Agent 加前置/后置处理,而不是把它塞进静态图里。


三、Graph 与 Tool 的相似性

反过来看,为什么“Graph 作为工具”就自然得多?

维度GraphTool
输入结构化数据结构化数据(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)

它内部处理了这几件事:

  1. 由输入类型推导工具的 JSON Schema。
  2. 把模型给的 JSON 参数反序列化成I。
  3. 编译并执行图。
  4. 处理中断(图内也可能有 HITL 点)。
  5. 把输出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 日:

  1. Agent 还是 Graph?AI 应用路线辨析
  2. Eino 官方示例:graphtool
  3. Eino 主仓库

本文示例按Eino v0.9.21编写,图内节点使用自定义 Lambda(不访问外部服务),已实机运行,正文输出为真实结果。官方graphtool包的签名与职责依据示例仓库文档整理,未在本篇实机运行。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进