Eino 学习第二阶段:ReAct Agent实战训练手册
📚 手册简介
欢迎进入第二阶段的学习!在第一阶段,你已经掌握了Eino的基础组件和Chain编排,实现了一个检索关键词生成器。现在,我们将迈向真正的智能体开发——让AI不仅会"思考",还能"行动"。
根据我们之前规划的路线图,第二阶段的目标是:
- ✅ ReAct模式理解:掌握"思考-行动-观察"的循环机制
- ✅ ToolsNode开发:实现专利场景的自定义工具
- ✅ 构建单一角色Agent:完成"检索分析员"智能体
按照本手册,你将在3-4小时内完成全部学习任务,获得一个能自主工作的专利检索分析员Agent——它会根据你的发明点描述,自主决定何时调用检索工具、如何分析结果、最终生成结构化检索报告。
第一部分:ReAct模式深度理解(45分钟)
1.1 什么是ReAct?
ReAct = Reason + Act,即"推理"与"行动"的协同。传统的AI只能"思考"然后"回答",而ReAct模式让AI能够:
- 思考(Think):分析当前任务,决定下一步做什么
- 行动(Act):调用工具执行具体操作(如检索专利、计算相似度)
- 观察(Observe):获取工具执行结果,并基于新信息继续思考
这个循环持续进行,直到任务完成。
用专利检索场景来理解:
用户输入:"帮我检索关于'区块链供应链金融'的专利"
┌─────────────────────────────────────┐
│ 第1轮思考:用户需要检索专利,我需要 │
│ 调用检索工具,关键词是"区块链供应链金融"│
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 第1轮行动:调用 patent_search tool │
│ 参数:{"keywords": "区块链供应链金融"} │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 第1轮观察:工具返回10篇相关专利 │
│ 专利1:... 专利2:... │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 第2轮思考:发现这些专利多涉及"智能合约"│
│ 需要进一步分析区别技术特征... │
└─────────────────────────────────────┘
↓
(继续循环)
1.2 Eino中ReAct Agent的架构
在Eino中,ReAct Agent底层使用compose.Graph进行编排,包含两个核心节点:ChatModel和ToolsNode。
┌─────────────────┐
│ 用户输入 │
└────────┬────────┘
↓
┌─────────────────────────────────────────────────┐
│ ReAct Agent │
│ ┌───────────────────────────────────────────┐ │
│ │ MessageModifier │ │ ← 可选:添加系统提示词
│ └───────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────┐ │
│ │ ChatModel │ │ ← 思考:决定是否调用工具
│ └───────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────┐ │
│ ↓ 有工具调用? ↓ 无工具调用 │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ ToolsNode │ │ 直接返回 │ │
│ │ (执行工具) │ │ 最终结果 │ │
│ └───────┬───────┘ └───────────────┘ │
│ ↓ │
│ 观察结果 │
│ ↓ │
│ 返回ChatModel(继续循环) │
└─────────────────────────────────────────────────┘
关键组件说明:
| 组件 | 作用 | 在专利场景中的角色 |
|---|---|---|
| ChatModel | 推理决策的大脑,必须支持Tool Calling能力 | 专利分析师,决定何时检索、如何分析 |
| ToolsNode | 执行具体的工具调用,管理多个工具 | 执行专利检索、格式检查、相似度计算 |
| MessageModifier | 在每次调用模型前修改消息,可添加持久化的系统提示 | 固定Agent的角色设定,如"你是一名资深专利代理人" |
| MaxStep | 控制最大循环步数,防止死循环 | 限制检索分析的最大轮数,默认12步约可执行5次工具调用 |
1.3 动手实践:运行第一个ReAct Agent示例
在深入编码前,我们先运行Eino官方提供的ReAct Agent示例,感受一下它的工作流程。
# 克隆官方示例仓库(如果还没克隆)
git clone https://github.com/cloudwego/eino-examples.git
cd eino-examples/flow/agent/react
# 查看示例代码
cat react_agent_example.go
运行示例:
go run react_agent_example.go
观察输出,你会看到类似这样的日志:
=== ReAct Agent Execution Trace ===
Step 1: Model thinking...
[思考] 用户想查询天气,我需要调用weather tool
Step 1: Calling tool "get_weather" with args: {"location":"Beijing"}
Step 1: Tool returned: "Beijing: 18°C, sunny"
Step 2: Model thinking...
[思考] 已经获取到天气信息,可以直接回答用户
Step 2: Final response: "北京今天天气晴朗,气温18摄氏度。"
这就是ReAct的完整工作流程!现在,让我们把这种能力应用到专利检索场景中。
第二部分:Tool开发实战——实现专利检索工具(60分钟)
在专利代写场景中,最核心的工具就是专利检索。我们将实现一个模拟的专利检索工具,它虽然不调用真实的专利数据库,但完全模拟了真实的调用流程和数据结构。
2.1 工具定义方式对比
Eino提供了三种实现Tool的方式:
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
utils.NewTool |
简单工具,参数少 | 直观易懂 | 参数信息需要手动定义,与结构体分离 |
utils.InferTool |
推荐!参数复杂的工具 | 参数结构体与描述同源,维护简单 | 需要理解JSON Schema tag |
实现tool.BaseTool接口 |
需要复杂自定义逻辑 | 最灵活,完全控制 | 代码量较大 |
对于专利检索工具,参数较多(关键词、分类号、日期范围等),我们采用**InferTool方式**,这是最优雅的实践。
2.2 创建专利检索工具
创建pkg/tool/patent_search.go:
package tool
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/components/tool/utils"
"github.com/gogf/gf/v2/text/gstr"
)
// PatentSearchParams 专利检索参数结构体
// 使用 JSON Schema tag 定义参数描述,实现"参数与描述同源"
type PatentSearchParams struct {
Keywords string `json:"keywords" jsonschema:"description=核心关键词,多个关键词用逗号隔开,required"`
IpcClasses []string `json:"ipc_classes,omitempty" jsonschema:"description=IPC分类号列表,如['G06F','H04L']"`
DateFrom string `json:"date_from,omitempty" jsonschema:"description=申请日期起始,格式:YYYY-MM-DD"`
DateEnd string `json:"date_end,omitempty" jsonschema:"description=申请日期截止,格式:YYYY-MM-DD"`
Assignee string `json:"assignee,omitempty" jsonschema:"description=申请人/专利权人"`
MaxResults uint `json:"max_results,omitempty" jsonschema:"description=最大返回结果数,默认5"`
}
// PatentInfo 专利信息结构体
type PatentInfo struct {
PatentID string `json:"patent_id" description:"专利号/公开号"`
Title string `json:"title" description:"标题"`
Abstract string `json:"abstract" description:"摘要"`
Applicants []string `json:"applicants" description:"申请人"`
Inventors []string `json:"inventors" description:"发明人"`
ApplicationDate string `json:"application_date" description:"申请日"`
PublicationDate string `json:"publication_date" description:"公开日"`
IpcClasses []string `json:"ipc_classes" description:"IPC分类号"`
}
// PatentSearchResult 检索结果
type PatentSearchResult struct {
TotalCount uint `json:"total_count" description:"总命中数"`
Patents []PatentInfo `json:"patents" description:"专利列表"`
SearchID string `json:"search_id" description:"检索ID,用于追踪"`
}
// 模拟专利数据库(实际项目中应替换为真实API调用)
var mockPatentDB = []PatentInfo{
{
PatentID: "CN202310123456A",
Title: "基于区块链的供应链金融风控方法及系统",
Abstract: "本发明公开了一种基于区块链的供应链金融风控方法,包括:获取供应链交易数据;通过智能合约验证交易真实性;基于验证结果动态调整信用额度...",
Applicants: []string{"蚂蚁科技集团股份有限公司"},
Inventors: []string{"张三", "李四"},
ApplicationDate: "2023-05-15",
PublicationDate: "2023-09-20",
IpcClasses: []string{"G06Q 40/02", "G06F 21/64"},
},
{
PatentID: "CN202210789012A",
Title: "区块链节点信用评价方法及装置",
Abstract: "本申请涉及一种区块链节点信用评价方法,通过智能合约收集节点交易行为数据,基于预设算法计算节点信用分...",
Applicants: []string{"腾讯科技(深圳)有限公司"},
Inventors: []string{"王五", "赵六"},
ApplicationDate: "2022-08-20",
PublicationDate: "2023-02-10",
IpcClasses: []string{"G06Q 40/04", "H04L 29/08"},
},
// 更多模拟数据...
}
// PatentSearchFunc 是专利检索工具的执行函数
func PatentSearchFunc(ctx context.Context, params *PatentSearchParams) (string, error) {
// 参数验证
if params.Keywords == "" {
return "", fmt.Errorf("关键词不能为空")
}
// 设置默认值
maxResults := params.MaxResults
if maxResults <= 0 {
maxResults = 5
}
// 模拟检索过程(实际应调用专利数据库API)
// 这里简单实现:根据关键词模糊匹配标题和摘要
var results []PatentInfo
for _, patent := range mockPatentDB {
// 简单的关键词匹配(实际应使用全文检索)
if gstr.Contains(patent.Title, params.Keywords) ||
gstr.Contains(patent.Abstract, params.Keywords) {
results = append(results, patent)
}
// 如果达到最大结果数,停止
if uint(len(results)) >= maxResults {
break
}
}
// 构造返回结果
searchResult := PatentSearchResult{
TotalCount: uint(len(results)),
Patents: results,
SearchID: fmt.Sprintf("search_%d", time.Now().UnixNano()),
}
// 序列化为JSON字符串(工具必须返回string)
resultJSON, err := json.MarshalIndent(searchResult, "", " ")
if err != nil {
return "", fmt.Errorf("序列化结果失败: %v", err)
}
return string(resultJSON), nil
}
// NewPatentSearchTool 使用InferTool创建专利检索工具
// InferTool会根据参数结构体的json tag自动生成ToolInfo,实现"同源维护"
func NewPatentSearchTool() (tool.InvokableTool, error) {
newTool, err := utils.InferTool(
"patent_search", // 工具名称
"检索专利数据库,根据关键词、IPC分类号、日期范围等条件查找相关专利", // 工具描述
PatentSearchFunc, // 执行函数
)
if err != nil {
return nil, fmt.Errorf("创建专利检索工具失败: %v", err)
}
return newTool, nil
}
2.3 创建辅助工具:专利格式检查器
除了检索,我们的"检索分析员"还需要检查权利要求格式。再创建一个辅助工具:
创建pkg/tools/claim_checker.go:
package tools
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/cloudwego/eino/components/tool/utils"
)
// ClaimCheckParams 权利要求检查参数
type ClaimCheckParams struct {
ClaimsText string `json:"claims_text" jsonschema:"description=权利要求的全文,包含所有权利要求项"`
}
// ClaimIssue 权利要求缺陷
type ClaimIssue struct {
ClaimNumber string `json:"claim_number"` // 权利要求序号,如"1"、"2"
IssueType string `json:"issue_type"` // 缺陷类型:格式错误/支持不足/不清楚
Description string `json:"description"` // 缺陷描述
Suggestion string `json:"suggestion"` // 修改建议
}
// ClaimCheckResult 检查结果
type ClaimCheckResult struct {
IsValid bool `json:"is_valid"` // 是否通过检查
Issues []ClaimIssue `json:"issues"` // 缺陷列表
TotalCount int `json:"total_count"` // 权利要求总数
}
// ClaimCheckerFunc 权利要求格式检查函数
func ClaimCheckerFunc(ctx context.Context, params *ClaimCheckParams) (string, error) {
if params.ClaimsText == "" {
return "", fmt.Errorf("权利要求文本不能为空")
}
// 按行分割,简单识别每个权利要求
lines := strings.Split(params.ClaimsText, "\n")
var issues []ClaimIssue
var currentClaimNum string
// 简单的格式检查规则(实际应使用更复杂的NLP分析)
for i, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// 识别权利要求序号(如"1."、"2、")
if strings.ContainsAny(line, ".、") {
parts := strings.SplitN(line, ".", 2)
if len(parts) == 2 && isNumeric(parts[0]) {
currentClaimNum = parts[0]
// 检查独立权利要求(通常序号1)是否包含必要特征
if currentClaimNum == "1" && !strings.Contains(line, "其特征在于") {
issues = append(issues, ClaimIssue{
ClaimNumber: currentClaimNum,
IssueType: "格式错误",
Description: "独立权利要求应包含'其特征在于'等区分前序部分与特征部分的用语",
Suggestion: "建议在技术特征前添加'其特征在于'",
})
}
// 检查从属权利要求引用基础
if currentClaimNum != "1" && !strings.Contains(line, "根据权利要求") {
issues = append(issues, ClaimIssue{
ClaimNumber: currentClaimNum,
IssueType: "格式错误",
Description: "从属权利要求应当写明引用的权利要求编号",
Suggestion: "建议以'根据权利要求X所述的...'开头",
})
}
}
}
}
result := ClaimCheckResult{
IsValid: len(issues) == 0,
Issues: issues,
TotalCount: len(lines),
}
resultJSON, err := json.MarshalIndent(result, "", " ")
if err != nil {
return "", err
}
return string(resultJSON), nil
}
// 辅助函数:判断字符串是否全是数字
func isNumeric(s string) bool {
for _, c := range s {
if c < '0' || c > '9' {
return false
}
}
return true
}
// NewClaimCheckerTool 创建权利要求检查工具
func NewClaimCheckerTool() (*utils.InferTool, error) {
return utils.InferTool(
"claim_format_checker",
"检查权利要求的格式是否符合专利法要求,包括引用关系、特征部分用语等",
ClaimCheckerFunc,
)
}
2.4 测试工具独立运行
在集成到Agent之前,先单独测试工具是否能正常工作:
创建cmd/tools_test/tools_test.go:
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"patent-agent-learning/pkg/tools"
)
func main() {
ctx := context.Background()
// 测试专利检索工具
fmt.Println("===== 测试专利检索工具 =====")
searchTool, err := tools.NewPatentSearchTool()
if err != nil {
log.Fatal(err)
}
// 获取工具信息(查看生成的参数描述)
info, err := searchTool.Info(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("工具名称: %s\n", info.Name)
fmt.Printf("工具描述: %s\n", info.Desc)
fmt.Printf("参数定义: %+v\n", info.ParamsOneOf)
// 调用工具
result, err := searchTool.InvokableRun()(ctx, `{"keywords": "区块链供应链金融", "max_results": 3}`)
if err != nil {
log.Fatal(err)
}
// 美化输出
var prettyJSON map[string]interface{}
json.Unmarshal([]byte(result), &prettyJSON)
pretty, _ := json.MarshalIndent(prettyJSON, "", " ")
fmt.Printf("检索结果:\n%s\n", pretty)
// 测试权利要求检查工具
fmt.Println("\n===== 测试权利要求检查工具 =====")
checkTool, err := tools.NewClaimCheckerTool()
if err != nil {
log.Fatal(err)
}
claimText := `1. 一种基于区块链的供应链金融方法,包括:
- 获取供应链交易数据;
- 将交易数据上链存储。
2. 如权利要求1所述的方法,还包括验证交易真实性。
3. 一种计算机可读存储介质。`
checkResult, err := checkTool.InvokableRun()(ctx, fmt.Sprintf(`{"claims_text": %q}`, claimText))
if err != nil {
log.Fatal(err)
}
fmt.Printf("检查结果:\n%s\n", checkResult)
}
运行测试:
go run cmd/tools_test/tools_test.go
第三部分:构建第一个专利检索分析员Agent(75分钟)
现在,我们将前面学习的ReAct模式和自定义工具结合起来,构建第一个完整的专业Agent——专利检索分析员。
3.1 理解ChatModelAgent的配置
Eino ADK提供了adk.NewChatModelAgent来快速创建基于ReAct模式的智能体。核心配置项包括:
| 配置项 | 作用 | 专利场景设置建议 |
|---|---|---|
| Name | Agent名称 | “PatentRetrievalAnalyst” |
| Instruction | 系统提示词,定义Agent行为 | 设定角色、工作流程、输出规范 |
| Model | 支持Tool Calling的ChatModel | OpenAI GPT-4或同级别模型 |
| ToolsConfig | 可用的工具列表 | 专利检索工具+格式检查工具 |
| MaxIterations | 最大循环次数,防止死循环 | 15-20次足够完成复杂检索分析 |
3.2 实现检索分析员Agent
创建pkg/agents/retrieval_analyst.go:
package agents
import (
"context"
"fmt"
"log"
"os"
"github.com/cloudwego/eino-ext/components/model/openai"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
"patent-agent-learning/pkg/tools"
)
// NewRetrievalAnalystAgent 创建专利检索分析员Agent
func NewRetrievalAnalystAgent(ctx context.Context) (adk.Agent, error) {
// 1. 初始化ChatModel(必须支持Tool Calling)
chatModel, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
Model: os.Getenv("OPENAI_MODEL"), // 推荐使用gpt-4或支持tool calling的模型
APIKey: os.Getenv("OPENAI_API_KEY"),
})
if err != nil {
return nil, fmt.Errorf("初始化ChatModel失败: %v", err)
}
// 2. 创建所需工具
patentSearchTool, err := tools.NewPatentSearchTool()
if err != nil {
return nil, fmt.Errorf("创建专利检索工具失败: %v", err)
}
claimCheckerTool, err := tools.NewClaimCheckerTool()
if err != nil {
return nil, fmt.Errorf("创建权利要求检查工具失败: %v", err)
}
// 工具列表
toolList := []tool.BaseTool{
patentSearchTool,
claimCheckerTool,
}
// 3. 获取工具信息并绑定到ChatModel(ReAct Agent需要)
toolInfos := make([]*schema.ToolInfo, 0, len(toolList))
for _, t := range toolList {
info, err := t.Info(ctx)
if err != nil {
return nil, fmt.Errorf("获取工具信息失败: %v", err)
}
toolInfos = append(toolInfos, info)
}
// 将工具绑定到模型(返回新的模型实例,原模型不变,线程安全)
toolCallingModel, err := chatModel.WithTools(toolInfos)
if err != nil {
return nil, fmt.Errorf("绑定工具到模型失败: %v", err)
}
// 4. 使用adk创建ChatModelAgent
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "PatentRetrievalAnalyst",
Description: "专利检索分析专家,能够进行专利检索、分析区别技术特征、检查权利要求格式",
// Instruction是系统提示词,定义Agent的行为模式
Instruction: `你是一位资深的专利检索分析师,拥有10年专利审查和检索经验。
你的工作流程如下:
1. **理解需求**:首先理解用户提供的发明点描述或技术交底书。
2. **规划检索策略**:思考需要检索哪些关键词、哪些IPC分类号。
3. **执行检索**:调用 patent_search 工具进行检索。如果第一次结果不理想,尝试调整关键词重新检索。
4. **分析结果**:阅读检索到的专利,提取现有技术,分析区别技术特征。
5. **生成报告**:输出结构化的检索分析报告,包含:
- 检索策略(使用的关键词、分类号)
- 对比文件列表(最相关的3-5篇)
- 区别技术特征分析
- 后续撰写建议
如果需要检查权利要求格式,可以调用 claim_format_checker 工具。
请始终保持专业、严谨的分析态度。`,
Model: toolCallingModel, // 使用绑定了工具的模型
ToolsConfig: adk.ToolsConfig{
Tools: toolList,
},
MaxIterations: 15, // 防止死循环
})
if err != nil {
return nil, fmt.Errorf("创建Agent失败: %v", err)
}
return agent, nil
}
3.3 运行检索分析员Agent
创建cmd/retrieval_analyst/main.go:
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/joho/godotenv"
"github.com/cloudwego/eino/schema"
"patent-agent-learning/pkg/agents"
)
func main() {
// 加载环境变量
if err := godotenv.Load(); err != nil {
log.Println("警告: 未找到.env文件,使用系统环境变量")
}
ctx := context.Background()
// 创建Agent
fmt.Println("正在初始化专利检索分析员Agent...")
agent, err := agents.NewRetrievalAnalystAgent(ctx)
if err != nil {
log.Fatalf("创建Agent失败: %v", err)
}
// 准备用户输入
inventionPoint := `本发明涉及一种基于区块链的供应链金融风控方法,特别是一种利用智能合约自动验证供应链交易的真实性,并基于验证结果动态调整参与企业的信用额度和融资利率的技术。`
userMessage := schema.UserMessage(fmt.Sprintf(`
请为我分析以下发明点,进行专利检索,并生成检索分析报告:
发明点描述:
%s
要求:
1. 检索相关专利
2. 分析现有技术
3. 找出区别技术特征
4. 给出撰写建议
`, inventionPoint))
fmt.Println("\n===== 开始执行检索分析 =====")
fmt.Printf("用户输入: %s\n", userMessage.Content)
fmt.Println("Agent开始思考...")
fmt.Println("(这可能需要30-60秒,请耐心等待)\n")
startTime := time.Now()
// 运行Agent(非流式调用)
input := &adk.AgentInput{
Messages: []*schema.Message{userMessage},
}
iter := agent.Run(ctx, input)
// 处理输出事件
var finalResponse string
for {
event, ok := iter.Next()
if !ok {
break
}
// 如果有错误,打印并退出
if event.Err != nil {
log.Printf("Agent执行出错: %v", event.Err)
break
}
// 如果有输出内容
if event.Output != nil && event.Output.MessageOutput != nil {
if !event.Output.MessageOutput.IsStreaming {
// 非流式输出
if event.Output.MessageOutput.Message != nil {
content := event.Output.MessageOutput.Message.Content
if content != "" {
finalResponse += content
}
}
}
}
// 如果有Action,可以打印调试信息
if event.Action != nil {
if event.Action.TransferToAgent != nil {
fmt.Printf("🔄 任务移交给: %s\n", event.Action.TransferToAgent.DestAgentName)
}
}
}
elapsed := time.Since(startTime)
fmt.Println("\n===== 检索分析完成 =====")
fmt.Printf("耗时: %v\n", elapsed)
fmt.Printf("\n%s\n", finalResponse)
// 可选:尝试流式输出体验
fmt.Println("\n===== 体验流式输出 =====")
fmt.Println("正在重新运行(流式模式)...")
input.EnableStreaming = true
streamIter := agent.Run(ctx, input)
fmt.Print("Agent: ")
for {
event, ok := streamIter.Next()
if !ok {
break
}
if event.Output != nil && event.Output.MessageOutput != nil && event.Output.MessageOutput.IsStreaming {
// 流式输出,逐块打印
chunk, err := event.Output.MessageOutput.MessageStream.Recv()
if err == nil {
fmt.Print(chunk.Content)
}
}
}
fmt.Println()
}
3.4 配置文件
创建.env文件(如果还没有):
# OpenAI配置
OPENAI_API_KEY=你的OpenAI密钥
OPENAI_MODEL=gpt-4o # 必须支持tool calling,推荐gpt-4或gpt-4o
# 如果使用Ollama本地模型(可选)
# OLLAMA_BASE_URL=http://localhost:11434
# OLLAMA_MODEL=llama3.1 # 需要支持tool calling的模型
关于模型选择的说明:ReAct Agent要求模型必须支持Tool Calling功能。目前支持该能力的模型包括:
- OpenAI: gpt-4, gpt-4o, gpt-3.5-turbo
- Anthropic Claude系列
- 字节跳动的Doubao(通过Ark API)
- 部分通过Ollama部署的开源模型(如llama3.1)
如果不确定模型是否支持,可以先运行示例,观察是否出现工具调用相关的日志。
第四部分:理解Agent的协作机制与状态管理(30分钟)
4.1 ReAct Agent的执行流程追踪
为了更好地理解Agent内部发生了什么,我们添加一些调试输出。创建一个调试版本:
创建cmd/retrieval_analyst/debug_main.go:
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"time"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/schema"
"github.com/joho/godotenv"
"patent-agent-learning/pkg/agents"
)
// debugEventHandler 自定义事件处理器,打印详细执行过程
func debugEventHandler(event *adk.AgentEvent) {
if event == nil {
return
}
timestamp := time.Now().Format("15:04:05.000")
// 打印事件来源
fmt.Printf("\n[%s] [%s] ", timestamp, event.AgentName)
// 打印Action(工具调用、转移等)
if event.Action != nil {
if event.Action.Exit {
fmt.Println("🔚 Agent退出")
return
}
if event.Action.TransferToAgent != nil {
fmt.Printf("🔄 转移任务至: %s\n", event.Action.TransferToAgent.DestAgentName)
return
}
// 如果是工具调用,这里可能通过其他方式体现
}
// 打印输出内容
if event.Output != nil && event.Output.MessageOutput != nil {
msg := event.Output.MessageOutput.Message
if msg != nil {
// 检查是否是工具调用请求
if len(msg.ToolCalls) > 0 {
fmt.Printf("🤔 思考: 决定调用工具\n")
for _, tc := range msg.ToolCalls {
fmt.Printf(" 🛠️ 调用工具: %s\n", tc.Function.Name)
fmt.Printf(" 参数: %s\n", tc.Function.Arguments)
}
} else if msg.Content != "" {
// 普通文本输出
fmt.Printf("💬 输出: %.100s...\n", msg.Content)
}
}
}
// 如果有错误
if event.Err != nil {
fmt.Printf("❌ 错误: %v\n", event.Err)
}
}
func main() {
godotenv.Load()
ctx := context.Background()
fmt.Println("=== 专利检索分析员Agent(调试模式)===")
agent, err := agents.NewRetrievalAnalystAgent(ctx)
if err != nil {
log.Fatal(err)
}
inventionPoint := `一种基于深度学习的医学图像分割方法,利用改进的U-Net架构对CT影像中的肺结节进行自动分割和分类。`
input := &adk.AgentInput{
Messages: []*schema.Message{
schema.UserMessage(fmt.Sprintf("请分析这个发明点,检索相关专利:%s", inventionPoint)),
},
}
fmt.Println("\n📥 用户输入:", inventionPoint)
fmt.Println("\n=== Agent执行轨迹 ===\n")
iter := agent.Run(ctx, input)
for {
event, ok := iter.Next()
if !ok {
break
}
debugEventHandler(event)
}
fmt.Println("\n=== 执行结束 ===")
}
运行调试版本,观察Agent的思考轨迹:
go run cmd/retrieval_analyst/debug_main.go
你会看到类似这样的输出,完整展示了ReAct循环的每个步骤:
=== Agent执行轨迹 ===
[15:23:45.123] [PatentRetrievalAnalyst] 🤔 思考: 决定调用工具
🛠️ 调用工具: patent_search
参数: {"keywords":"深度学习 医学图像分割 U-Net 肺结节","max_results":5}
[15:23:47.456] [PatentRetrievalAnalyst] 💬 输出: 已检索到5篇相关专利,正在分析区别技术特征...
[15:23:49.789] [PatentRetrievalAnalyst] 🤔 思考: 决定调用工具
🛠️ 调用工具: patent_search
参数: {"keywords":"CT影像 肺结节 分割 CNN","max_results":3}
[15:23:52.012] [PatentRetrievalAnalyst] 💬 输出: 根据两轮检索结果,我发现最接近的现有技术是...
4.2 Session状态管理
在复杂的多轮交互中,Agent需要记住之前的信息。Eino ADK提供了Session机制来实现状态共享。
在Plan-Execute模式中,Session的作用尤为明显:
- Planner:将计划写入Session
- Executor:从Session读取计划并执行
- Replanner:更新Session中的计划
虽然我们目前只实现了单Agent,但了解Session机制对后续多Agent协作很重要:
// 在Agent中获取Session值
func someAgentFunction(ctx context.Context) {
// 获取整个Session
sessionValues := adk.GetSessionValues(ctx)
// 获取特定键的值
plan, exists := adk.GetSessionValue(ctx, "retrieval_plan")
// 添加值到Session
adk.AddSessionValue(ctx, "search_results", results)
// 批量添加
adk.AddSessionValues(ctx, map[string]any{
"keywords_used": keywords,
"search_id": searchID,
})
}
第五部分:进阶练习与探索(30分钟)
完成核心任务后,尝试以下扩展练习来巩固所学:
5.1 练习1:添加本地Ollama支持
如果你想保护专利数据的隐私,尝试改用本地部署的Ollama:
import "github.com/cloudwego/eino-ext/components/model/ollama"
// 创建Ollama ChatModel
ollamaModel, err := ollama.NewChatModel(ctx, &ollama.ChatModelConfig{
BaseURL: os.Getenv("OLLAMA_BASE_URL"),
Model: os.Getenv("OLLAMA_MODEL"), // 需要支持tool calling,如llama3.1
})
注意:不是所有Ollama模型都支持Tool Calling,请确认模型能力。
5.2 练习2:添加真实专利检索API
将模拟的patent_search工具升级为调用真实API。以Google Patents Public Data或智慧芽API为例:
func PatentSearchFunc(ctx context.Context, params *PatentSearchParams) (string, error) {
// 构建HTTP请求
client := &http.Client{Timeout: 30 * time.Second}
// 调用真实API
reqURL := fmt.Sprintf("https://api.patents.com/search?q=%s&limit=%d",
url.QueryEscape(params.Keywords), params.MaxResults)
resp, err := client.Get(reqURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
// 解析响应
var apiResult PatentSearchResult
if err := json.NewDecoder(resp.Body).Decode(&apiResult); err != nil {
return "", err
}
// 返回结果
resultJSON, _ := json.Marshal(apiResult)
return string(resultJSON), nil
}
5.3 练习3:探索官方Excel Agent示例
Eino官方提供了一个完整的Excel Agent示例,它使用了与我们类似的技术栈(ReAct、Plan-Execute、Workflow Agents)。这个示例非常值得深入研究:
cd eino-examples/adk/excel_agent
go run main.go
重点关注:
- 如何组织多Agent协作
- 如何使用Plan-Execute模式
- 如何实现CodeAgent(代码生成与执行)
5.4 练习4:尝试eino-cli工具
社区开发的eino-cli是一个基于Eino的命令行Agent工具,支持多种模型和内置工具。可以作为参考实现:
# 安装
go install github.com/tk103331/eino-cli@latest
# 配置
cp ~/go/pkg/mod/github.com/tk103331/eino-cli@版本/config.yml.example ~/.eino-cli/config.yml
# 运行Agent
eino-cli agent --model gpt4 --tools duckduckgo_search
第六部分:常见问题排查
6.1 模型不支持Tool Calling
现象:Agent执行时,模型始终不调用工具,直接返回文本。 解决:
- 确认使用的模型支持Tool Calling(gpt-4, gpt-4o, claude-3等)
- 检查是否正确调用了
WithTools方法绑定工具 - 查看模型返回的日志,确认是否收到工具信息
6.2 Agent陷入死循环
现象:Agent不停地调用工具,从不返回最终结果。 解决:
- 设置
MaxIterations/MaxStep参数,限制最大循环次数 - 检查工具返回的结果格式,确保模型能正确理解
- 在Instruction中明确告诉模型何时应该结束
6.3 工具参数解析错误
现象:模型调用了工具,但报错"invalid argument"。 解决:
- 检查工具参数结构体的JSON tag是否正确
- 确保使用了
utils.InferTool,让框架自动生成参数Schema - 在Instruction中给出参数示例,帮助模型理解
6.4 流式输出不显示
现象:使用Stream模式但没有看到逐字输出。 解决:
- 确认正确设置了
EnableStreaming: true - 正确处理
MessageStream的Recv循环 - 检查控制台是否支持实时刷新(可能需要
fmt.Print而不是Println)
第七部分:阶段成果验收清单
完成本手册后,请对照清单检查是否达成学习目标:
| 任务 | 完成情况 | 备注 |
|---|---|---|
| ✅ 理解ReAct模式:能解释"思考-行动-观察"循环 | □ | |
| ✅ 掌握Tool开发:能用InferTool创建专利检索工具 | □ | |
| ✅ 掌握Tool开发:能用InferTool创建权利要求检查工具 | □ | |
| ✅ 理解模型绑定:知道如何用WithTools将工具绑定到模型 | □ | |
| ✅ 创建ChatModelAgent:成功构建"专利检索分析员" | □ | |
| ✅ 运行完整Agent:Agent能自主完成检索-分析-报告流程 | □ | |
| ✅ 体验流式输出:看到Agent逐字输出的效果 | □ | |
| ✅ 调试执行轨迹:能观察ReAct的每一步决策 | □ | |
| ✅ 尝试进阶练习:至少完成一个扩展练习 | □ |
代码成果:你拥有了第一个能自主工作的专业Agent——专利检索分析员,它能够:
- 理解发明点描述
- 自主决定何时调用检索工具
- 分析检索结果,提取区别技术特征
- 生成结构化检索分析报告
知识成果:你深入理解了ReAct模式的工作原理、Tool的开发与集成、ChatModelAgent的配置与运行,为第三阶段的多智能体协作打下了坚实基础。
预告:第三阶段——多智能体协作
恭喜你完成第二阶段的学习!在第三阶段,我们将:
- 学习Graph编排:用图的方式组织多个Agent
- 掌握预构建多Agent模式:Plan-Execute、Supervisor等
- 实现完整专利代写团队:主管Agent + 检索员 + 权利要求撰写员 + 说明书撰写员 + 审核员
请确保你对本阶段的内容有充分理解,特别是ReAct循环和Tool的开发,它们是构建复杂多智能体系统的基础。
遇到问题? 参考以下资源:
