ChatModelAgent 是 Eino ADK 中的一个核心预构建 的 Agent,它封装了与大语言模型(LLM)进行交互、并支持使用工具来完成任务的复杂逻辑。

下面,我们将创建一个图书推荐 Agent,演示如何配置和使用 ChatModelAgent 。这个 Agent 将能够根据用户的输入推荐相关图书。

  • 工具定义

对图书推荐 Agent,需要一个根据能够根据用户要求(题材、评分等)检索图书的工具 book_search 利用 Eino 提供的工具方法可以方便地创建(可参考如何创建一个 tool ?):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import (
"context"
"log"

"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/components/tool/utils"
)

type BookSearchInput struct {
Genre string `json:"genre" jsonschema:"description=Preferred book genre,enum=fiction,enum=sci-fi,enum=mystery,enum=biography,enum=business"`
MaxPages int `json:"max_pages" jsonschema:"description=Maximum page length (0 for no limit)"`
MinRating int `json:"min_rating" jsonschema:"description=Minimum user rating (0-5 scale)"`
}

type BookSearchOutput struct {
Books []string
}

func NewBookRecommender() tool.InvokableTool {
bookSearchTool, err := utils.InferTool("search_book", "Search books based on user preferences", func(ctx context.Context, input *BookSearchInput) (output *BookSearchOutput, err error) {
// search code
// ...
return &BookSearchOutput{Books: []string{"God's blessing on this wonderful world!"}}, nil
})
if err != nil {
log.Fatalf("failed to create search book tool: %v", err)
}
return bookSearchTool
}
  • 创建 ChatModel

为 ChatModelAgent 创建 ChatModel,Eino 提供了多种 ChatModel 封装(如 openai、gemini、doubao 等,详见 Eino: ChatModel 使用说明),这里以 openai ChatModel 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import (
"context"
"fmt"
"log"
"os"

"github.com/cloudwego/eino-ext/components/model/openai"
"github.com/cloudwego/eino/components/model"
)

func NewChatModel() model.ToolCallingChatModel {
ctx := context.Background()
apiKey := os.Getenv("OPENAI_API_KEY")
openaiModel := os.Getenv("OPENAI_MODEL")

cm, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
APIKey: apiKey,
Model: openaiModel,
})
if err != nil {
log.Fatal(fmt.Errorf("failed to create chatmodel: %w", err))
}
return cm
}
  • 创建 ChatModelAgent

除了配置 ChatModel 和工具外,还需要配置描述 Agent 功能用途的 Name 和 Description,以及指示 ChatModel 的 Instruction,Instruction 最终会作为 system message 被传递给 ChatModel。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import (
"context"
"fmt"
"log"

"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
)

func NewBookRecommendAgent() adk.Agent {
ctx := context.Background()

a, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "BookRecommender",
Description: "An agent that can recommend books",
Instruction: `You are an expert book recommender. Based on the user's request, use the "search_book" tool to find relevant books. Finally, present the results to the user.`,
Model: NewChatModel(),
ToolsConfig: adk.ToolsConfig{
ToolsNodeConfig: compose.ToolsNodeConfig{
Tools: []tool.BaseTool{NewBookRecommender()},
},
},
})
if err != nil {
log.Fatal(fmt.Errorf("failed to create chatmodel: %w", err))
}

return a
}

通过 Runner 运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import (
"context"
"fmt"
"log"
"os"

"github.com/cloudwego/eino/adk"

"github.com/cloudwego/eino-examples/adk/intro/chatmodel/internal"
)

func main() {
ctx := context.Background()
a := internal.NewBookRecommendAgent()
runner := adk.NewRunner(ctx, adk.RunnerConfig{
Agent: a,
})
iter := runner.Query(ctx, "recommend a fiction book to me")
for {
event, ok := iter.Next()
if !ok {
break
}
if event.Err != nil {
log.Fatal(event.Err)
}
msg, err := event.Output.MessageOutput.GetMessage()
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nmessage:\n%v\n======", msg)
}
}

示例结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
message:
assistant:
tool_calls:
index[0]:{Index:0x1400021d160 ID:call_soznhv9d Type:function Function:{Name:search_book Arguments:{"genre":"fiction","max_pages":0,"min_rating":4}} Extra:map[]}

finish_reason: tool_calls
usage: &{242 {0} 34 276}
======
message:
tool: {"Books":["God's blessing on this wonderful world!"]}
tool_call_id: call_soznhv9d
tool_call_name: search_book
======
message:
assistant: I found a book that matches your preferences: "God's blessing on this wonderful world!". It is a fiction book with no page limit and has a minimum rating of 4.0.
finish_reason: stop
usage: &{300 {0} 39 339}
======

工具调用

ChatModelAgent 内使用了 ReAct 模式,该模式旨在通过让 ChatModel 进行显式的、一步一步的“思考”来解决复杂问题。为 ChatModelAgent 配置了工具后,它在内部的执行流程就遵循了 ReAct 模式:

  • 调用 ChatModel(Reason)
  • LLM 返回工具调用请求(Action)
  • ChatModelAgent 执行工具(Act)
  • 它将工具结果返回给 ChatModel(Observation),然后开始新的循环,直到 ChatModel 判断不需要调用 Tool 结束。

image-20250909123414659

可以通过 ToolsConfig 为 ChatModelAgent 配置 Tool:

1
2
3
4
5
6
7
8
9
// github.com/cloudwego/eino/adk/chatmodel.go

type ToolsConfig struct {
compose.ToolsNodeConfig

// Names of the tools that will make agent return directly when the tool is called.
// When multiple tools are called and more than one tool is in the return directly list, only the first one will be returned.
ReturnDirectly map[string]bool
}

ToolsConfig 复用了 Eino Graph ToolsNodeConfig,详细参考:Eino: ToolsNode&Tool 使用说明。额外提供了 ReturnDirectly 配置,ChatModelAgent 调用配置在 ReturnDirectly 中的 Tool 后会直接退出。

当没有配置工具时,ChatModelAgent 退化为一次 ChatModel 调用。

GenModelInput

ChatModelAgent 创建时可以配置 GenModelInput,Agent 被调用时会使用该方法生成 ChatModel 的初始输入:

1
type GenModelInput func(ctx context.Context, instruction string, input *AgentInput) ([]Message, error)

Agent 提供了默认的 GenModelInput 方法:

  1. 将 Instruction 作为 system message 加到 AgentInput.Messages 前
  2. 以 SessionValues 为 variables 渲染 1 中得到的 message list

OutputKey

ChatModelAgent 创建时可以配置 OutputKey,配置后 Agent 产生的最后一个 message 会被以设置的 OutputKey 为 key 添加到 SessionValues 中。

Exit

Exit 字段支持配置一个 Tool,当 LLM 调用这个工具后并执行后,ChatModelAgent 将直接退出,效果类似 ToolReturnDirectly。Eino ADK 提供了一个 ExitTool,用户可以直接使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// github.com/cloudwego/eino/adk/chatmodel.go

type ExitTool struct{}

func (et ExitTool) Info(_ context.Context) (*schema.ToolInfo, error) {
return ToolInfoExit, nil
}

func (et ExitTool) InvokableRun(ctx context.Context, argumentsInJSON string, _ ...tool.Option) (string, error) {
type exitParams struct {
FinalResult string `json:"final_result"`
}

params := &exitParams{}
err := sonic.UnmarshalString(argumentsInJSON, params)
if err != nil {
return "", err
}

err = SendToolGenAction(ctx, "exit", NewExitAction())
if err != nil {
return "", err
}

return params.FinalResult, nil
}

Transfer

ChatModelAgent 实现了 OnSubAgents 接口,使用 SetSubAgents 为 ChatModelAgent 设置父或子 Agent 后,ChatModelAgent 会增加一个 Transfer Tool,并且在 prompt 中指示 ChatModel 在需要 transfer 时调用这个 Tool 并以 transfer 目标 AgentName 作为 Tool 输入。在此工具被调用后,Agent 会产生 TransferAction 并退出。

AgentTool

ChatModelAgent 提供了工具方法,可以方便地将 Eino ADK Agent 转化为 Tool 供 ChatModelAgent 调用:

1
2
3
// github.com/cloudwego/eino/adk/agent_tool.go

func NewAgentTool(_ context.Context, agent Agent, options ...AgentToolOption) tool.BaseTool

比如之前创建的 BookRecommendAgent 可以使用 NewAgentTool 方法转换为 Tool,并被其他 Agent 调用:

1
2
3
4
5
6
7
8
9
10
11
12
bookRecommender := NewBookRecommendAgent()
bookRecommendeTool := NewAgentTool(ctx, bookRecommender)

// other agent
a, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
// xxx
ToolsConfig: adk.ToolsConfig{
ToolsNodeConfig: compose.ToolsNodeConfig{
Tools: []tool.BaseTool{bookRecommendeTool},
},
},
})

Interrupt&Resume

ChatModelAgent 支持 Interrupt&Resume,我们给 BookRecommendAgent 增加一个工具 ask_for_clarification,当用户提供的信息不足以支持推荐时,Agent 将调用这个工具向用户询问更多信息,ask_for_clarification 使用了 Interrupt&Resume 能力来实现向用户“询问”。

ChatModelAgent 使用了 Eino Graph 实现,在 agent 中可以复用 Eino Graph 的 Interrupt&Resume 能力,工具返回特殊错误使 Graph 触发中断并向外抛出自定义信息,在恢复时 Graph 会重新运行此工具:

1
2
3
// github.com/cloudwego/eino/adk/interrupt.go

func NewInterruptAndRerunErr(extra any) error

另外定义 ToolOption 来在恢复时传递新输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
import (
"github.com/cloudwego/eino/components/tool"
)

type askForClarificationOptions struct {
NewInput *string
}

func WithNewInput(input string) tool.Option {
return tool.WrapImplSpecificOptFn(func(t *askForClarificationOptions) {
t.NewInput = &input
})
}

💡 定义 tool option 不是必须的,实践时可以根据 context、闭包等其他方式传递新输入

完整的 Tool 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import (
"context"
"log"

"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/components/tool/utils"
"github.com/cloudwego/eino/compose"
)

type askForClarificationOptions struct {
NewInput *string
}

func WithNewInput(input string) tool.Option {
return tool.WrapImplSpecificOptFn(func(t *askForClarificationOptions) {
t.NewInput = &input
})
}

type AskForClarificationInput struct {
Question string `json:"question" jsonschema:"description=The specific question you want to ask the user to get the missing information"`
}

func NewAskForClarificationTool() tool.InvokableTool {
t, err := utils.InferOptionableTool(
"ask_for_clarification",
"Call this tool when the user's request is ambiguous or lacks the necessary information to proceed. Use it to ask a follow-up question to get the details you need, such as the book's genre, before you can use other tools effectively.",
func(ctx context.Context, input *AskForClarificationInput, opts ...tool.Option) (output string, err error) {
o := tool.GetImplSpecificOptions[askForClarificationOptions](nil, opts...)
if o.NewInput == nil {
return "", compose.NewInterruptAndRerunErr(input.Question)
}
return *o.NewInput, nil
})
if err != nil {
log.Fatal(err)
}
return t
}

ask_for_clarification 添加到之前的 Agent 中:

1
2
3
4
5
6
7
8
9
10
11
12
func NewBookRecommendAgent() adk.Agent {
// xxx
a, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
// xxx
ToolsConfig: adk.ToolsConfig{
ToolsNodeConfig: compose.ToolsNodeConfig{
Tools: []tool.BaseTool{NewBookRecommender(), NewAskForClarificationTool()},
},
},
})
// xxx
}

之后在 Runner 中配置 CheckPointStore(例子中使用最简单的 InMemoryStore),并在调用 Agent 时传入 CheckPointID,用来在恢复时使用。eino Graph 在中断时,会把 Graph 的 InterruptInfo 放入 Interrupted.Data 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func main() {
ctx := context.Background()
a := internal.NewBookRecommendAgent()
runner := adk.NewRunner(ctx, adk.RunnerConfig{
Agent: a,
CheckPointStore: newInMemoryStore(),
})
iter := runner.Query(ctx, "recommend a book to me", adk.WithCheckPointID("1"))
for {
event, ok := iter.Next()
if !ok {
break
}
if event.Err != nil {
log.Fatal(event.Err)
}
if event.Action != nil && event.Action.Interrupted != nil {
fmt.Printf("\ninterrupt happened, info: %+v\n", event.Action.Interrupted.Data.(*adk.ChatModelAgentInterruptInfo).Info.RerunNodesExtra["ToolNode"])
continue
}
msg, err := event.Output.MessageOutput.GetMessage()
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nmessage:\n%v\n======\n\n", msg)
}

// xxxxxx
}

可以在中断看到输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
name: BookRecommender
path: [{BookRecommender}]
tool name: ask_for_clarification
arguments: {"question":"Could you please specify the genre you're interested in, such as fiction, sci-fi, mystery, biography, or business?"}

name: BookRecommender
path: [{BookRecommender}]
action: interrupted
interrupt snapshot: {
"Info": {
"State": {
"Messages": [
{
"role": "system",
"content": "You are an expert book recommender. Based on the user's request, use the \"search_book\" tool to find relevant books. Finally, present the results to the user."
},
{
"role": "user",
"content": "recommend a book to me"
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"index": 0,
"id": "call_e7uebdb7",
"type": "function",
"function": {
"name": "ask_for_clarification",
"arguments": "{\"question\":\"Could you please specify the genre you're interested in, such as fiction, sci-fi, mystery, biography, or business?\"}"
}
}
],
"response_meta": {
"finish_reason": "tool_calls",
"usage": {
"prompt_tokens": 346,
"prompt_token_details": {
"cached_tokens": 0
},
"completion_tokens": 47,
"total_tokens": 393
}
}
}
],
"ReturnDirectlyToolCallID": "",
"ToolGenActions": {},
"AgentName": "BookRecommender",
"AgentToolInterruptData": {}
},
"BeforeNodes": null,
"AfterNodes": null,
"RerunNodes": [
"ToolNode"
],
"RerunNodesExtra": {
"ToolNode": {
"ToolCalls": [
{
"index": 0,
"id": "call_e7uebdb7",
"type": "function",
"function": {
"name": "ask_for_clarification",
"arguments": "{\"question\":\"Could you please specify the genre you're interested in, such as fiction, sci-fi, mystery, biography, or business?\"}"
}
}
],
"ExecutedTools": {},
"RerunTools": [
"call_e7uebdb7"
],
"RerunExtraMap": {
"call_e7uebdb7": "Could you please specify the genre you're interested in, such as fiction, sci-fi, mystery, biography, or business?"
}
}
},
"SubGraphs": {}
},
"Data": "/4p/AwEBCmNoZWNrcG9pbnQB/4AAAQcBCENoYW5uZWxzAf+CAAEGSW5wdXRzAf+EAAEFU3RhdGUBEAABDlNraXBQcmVIYW5kbGVyAf+GAAEKUmVydW5Ob2RlcwH/iAABFlRvb2xzTm9kZUV4ZWN1dGVkVG9vbHMB/4wAAQlTdWJHcmFwaHMB/44AAAAq/4EEAQEabWFwW3N0cmluZ11jb21wb3NlLmNoYW5uZWwB/4IAAQwBEAAAJ/+DBAEBF21hcFtzdHJpbmddaW50ZXJmYWNlIHt9Af+EAAEMARAAAB//hQQBAQ9tYXBbc3RyaW5nXWJvb2wB/4YAAQwBAgAAFv+HAgEBCFtdc3RyaW5nAf+IAAEMAAAt/4sEAQEcbWFwW3N0cmluZ11tYXBbc3RyaW5nXXN0cmluZwH/jAABDAH/igAADv+JBAEC/4oAAQwBDAAAL/+NBAEBHm1hcFtzdHJpbmddKmNvbXBvc2UuY2hlY2twb2ludAH/jgABDAH/gAAASv+AAQMJQ2hhdE1vZGVsFF9laW5vX3ByZWdlbF9jaGFubmVs/48DAQENcHJlZ2VsQ2hhbm5lbAH/kAABAQEGVmFsdWVzAf+EAAAA/97/kAMBAAAIVG9vbE5vZGUUX2Vpbm9fcHJlZ2VsX2NoYW5uZWz/kAMBAAADZW5kFF9laW5vX3ByZWdlbF9jaGFubmVs/5ADAQAAAQABFV9laW5vX2Fka19yZWFjdF9zdGF0Zf+RAwEBBVN0YXRlAf+SAAEFAQhNZXNzYWdlcwH/ugABGFJldHVybkRpcmVjdGx5VG9vbENhbGxJRAEMAAEOVG9vbEdlbkFjdGlvbnMB/8IAAQlBZ2VudE5hbWUBDAABFkFnZW50VG9vbEludGVycnVwdERhdGEB/9AAAAAg/7kCAQERW10qc2NoZW1hLk1lc3NhZ2UB/7oAAf+UAAD/mf+TAwEC/5QAAQoBBFJvbGUBDAABB0NvbnRlbnQBDAABDE11bHRpQ29udGVudAH/oAABBE5hbWUBDAABCVRvb2xDYWxscwH/pgABClRvb2xDYWxsSUQBDAABCFRvb2xOYW1lAQwAAQxSZXNwb25zZU1ldGEB/6gAARBSZWFzb25pbmdDb250ZW50AQwAAQVFeHRyYQH/hAAAACf/nwIBARhbXXNjaGVtYS5DaGF0TWVzc2FnZVBhcnQB/6AAAf+WAABm/5UDAQEPQ2hhdE1lc3NhZ2VQYXJ0Af+WAAEGAQRUeXBlAQwAAQRUZXh0AQwAAQhJbWFnZVVSTAH/mAABCEF1ZGlvVVJMAf+aAAEIVmlkZW9VUkwB/5wAAQdGaWxlVVJMAf+eAAAAVP+XAwEBE0NoYXRNZXNzYWdlSW1hZ2VVUkwB/5gAAQUBA1VSTAEMAAEDVVJJAQwAAQZEZXRhaWwBDAABCE1JTUVUeXBlAQwAAQVFeHRyYQH/hAAAAEn/mQMBARNDaGF0TWVzc2FnZUF1ZGlvVVJMAf+aAAEEAQNVUkwBDAABA1VSSQEMAAEITUlNRVR5cGUBDAABBUV4dHJhAf+EAAAASf+bAwEBE0NoYXRNZXNzYWdlVmlkZW9VUkwB/5wAAQQBA1VSTAEMAAEDVVJJAQwAAQhNSU1FVHlwZQEMAAEFRXh0cmEB/4QAAABR/50DAQESQ2hhdE1lc3NhZ2VGaWxlVVJMAf+eAAEFAQNVUkwBDAABA1VSSQEMAAEITUlNRVR5cGUBDAABBE5hbWUBDAABBUV4dHJhAf+EAAAAIP+lAgEBEVtdc2NoZW1hLlRvb2xDYWxsAf+mAAH/ogAASf+hAwEBCFRvb2xDYWxsAf+iAAEFAQVJbmRleAEEAAECSUQBDAABBFR5cGUBDAABCEZ1bmN0aW9uAf+kAAEFRXh0cmEB/4QAAAAx/6MDAQEMRnVuY3Rpb25DYWxsAf+kAAECAQROYW1lAQwAAQlBcmd1bWVudHMBDAAAAET/pwMBAQxSZXNwb25zZU1ldGEB/6gAAQMBDEZpbmlzaFJlYXNvbgEMAAEFVXNhZ2UB/6oAAQhMb2dQcm9icwH/rgAAAGb/qQMBAQpUb2tlblVzYWdlAf+qAAEEAQxQcm9tcHRUb2tlbnMBBAABElByb21wdFRva2VuRGV0YWlscwH/rAABEENvbXBsZXRpb25Ub2tlbnMBBAABC1RvdGFsVG9rZW5zAQQAAAAx/6sDAQESUHJvbXB0VG9rZW5EZXRhaWxzAf+sAAEBAQxDYWNoZWRUb2tlbnMBBAAAACP/rQMBAQhMb2dQcm9icwH/rgABAQEHQ29udGVudAH/uAAAAB//twIBARBbXXNjaGVtYS5Mb2dQcm9iAf+4AAH/sAAAR/+vAwEBB0xvZ1Byb2IB/7AAAQQBBVRva2VuAQwAAQdMb2dQcm9iAQgAAQVCeXRlcwH/sgABC1RvcExvZ1Byb2JzAf+2AAAAFf+xAgEBB1tdaW50NjQB/7IAAQQAACL/tQIBARNbXXNjaGVtYS5Ub3BMb2dQcm9iAf+2AAH/tAAAOf+zAwEBClRvcExvZ1Byb2IB/7QAAQMBBVRva2VuAQwAAQdMb2dQcm9iAQgAAQVCeXRlcwH/sgAAACz/wQQBARttYXBbc3RyaW5nXSphZGsuQWdlbnRBY3Rpb24B/8IAAQwB/7wAAFD/uwMBAv+8AAEEAQRFeGl0AQIAAQtJbnRlcnJ1cHRlZAH/vgABD1RyYW5zZmVyVG9BZ2VudAH/wAABEEN1c3RvbWl6ZWRBY3Rpb24BEAAAACT/vQMBAQ1JbnRlcnJ1cHRJbmZvAf++AAEBAQREYXRhARAAAAA1/78DAQEVVHJhbnNmZXJUb0FnZW50QWN0aW9uAf/AAAEBAQ1EZXN0QWdlbnROYW1lAQwAAAA3/88EAQEmbWFwW3N0cmluZ10qYWRrLmFnZW50VG9vbEludGVycnVwdEluZm8B/9AAAQwB/8QAACT/wwMBAv/EAAECAQlMYXN0RXZlbnQB/8YAAQREYXRhAQoAAABT/8UDAQEKQWdlbnRFdmVudAH/xgABBQEJQWdlbnROYW1lAQwAAQdSdW5QYXRoAf/KAAEGT3V0cHV0Af/MAAEGQWN0aW9uAf+8AAEDRXJyARAAAAAc/8kCAQENW11hZGsuUnVuU3RlcAH/ygAB/8gAABP/xwUBAQdSdW5TdGVwAf/IAAAAQf/LAwEBC0FnZW50T3V0cHV0Af/MAAECAQ1NZXNzYWdlT3V0cHV0Af/OAAEQQ3VzdG9taXplZE91dHB1dAEQAAAACv/NBQEC/9IAAABD/9MDAQE3U3RyZWFtUmVhZGVyWypnaXRodWIuY29tL2Nsb3Vkd2Vnby9laW5vL3NjaGVtYS5NZXNzYWdlXQH/1AAAAP4B4v+S/gHBAQMBBnN5c3RlbQH/nVlvdSBhcmUgYW4gZXhwZXJ0IGJvb2sgcmVjb21tZW5kZXIuIEJhc2VkIG9uIHRoZSB1c2VyJ3MgcmVxdWVzdCwgdXNlIHRoZSAic2VhcmNoX2Jvb2siIHRvb2wgdG8gZmluZCByZWxldmFudCBib29rcy4gRmluYWxseSwgcHJlc2VudCB0aGUgcmVzdWx0cyB0byB0aGUgdXNlci4AAQR1c2VyARZyZWNvbW1lbmQgYSBib29rIHRvIG1lAAEJYXNzaXN0YW50BAECDWNhbGxfZTd1ZWJkYjcBCGZ1bmN0aW9uAQEVYXNrX2Zvcl9jbGFyaWZpY2F0aW9uAf+BeyJxdWVzdGlvbiI6IkNvdWxkIHlvdSBwbGVhc2Ugc3BlY2lmeSB0aGUgZ2VucmUgeW91J3JlIGludGVyZXN0ZWQgaW4sIHN1Y2ggYXMgZmljdGlvbiwgc2NpLWZpLCBteXN0ZXJ5LCBiaW9ncmFwaHksIG9yIGJ1c2luZXNzPyJ9AAADAQp0b29sX2NhbGxzAQH+ArQBAAFeAf4DEgAAAAIAAQ9Cb29rUmVjb21tZW5kZXIBAAABAAEBCFRvb2xOb2RlAQEIVG9vbE5vZGUAAQAA"
}

之后向用户询问新输入并恢复运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func main() {
// xxx

scanner := bufio.NewScanner(os.Stdin)
fmt.Print("new input is:\n")
scanner.Scan()
nInput := scanner.Text()

iter, err := runner.Resume(ctx, "1", adk.WithToolOptions([]tool.Option{internal.WithNewInput(nInput)}))
if err != nil {
log.Fatal(err)
}
for {
event, ok := iter.Next()
if !ok {
break
}
if event.Err != nil {
log.Fatal(event.Err)
}
msg, err := event.Output.MessageOutput.GetMessage()
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nmessage:\n%v\n======\n\n", msg)
}
}

新的输出为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
new input is:
recommend me a fiction book
name: BookRecommender
path: [{BookRecommender}]
tool response: recommend me a fiction book

name: BookRecommender
path: [{BookRecommender}]
tool name: search_book
arguments: {"genre":"fiction","max_pages":0,"min_rating":4}

name: BookRecommender
path: [{BookRecommender}]
tool response: {"Books":["God's blessing on this wonderful world!"]}

name: BookRecommender
path: [{BookRecommender}]
answer: I found a book that matches your criteria. Here is one suggestion: "God's blessing on this wonderful world!" from the
fiction genre.

Would you like me to search for more options or provide some additional information?

最终代码

internal/agent.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package internal

import (
"context"
"fmt"
"log"
"os"

"github.com/cloudwego/eino-ext/components/model/openai"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
)

func init() {
os.Setenv("LLM_API_KEY", "sk-ollama")
os.Setenv("LLM_MODEL_NAME", "qwen2.5:7b")
os.Setenv("LLM_BASE_URL", "http://localhost:11434/v1")
}

func NewChatModel() model.ToolCallingChatModel {
ctx := context.Background()
apiKey := os.Getenv("LLM_API_KEY")
openaiModel := os.Getenv("LLM_MODEL_NAME")
baseUrl := os.Getenv("LLM_BASE_URL")

cm, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
APIKey: apiKey,
Model: openaiModel,
BaseURL: baseUrl,
})
if err != nil {
log.Fatal(fmt.Errorf("failed to create chatmodel: %w", err))
}
return cm
}

func NewBookRecommendAgent() adk.Agent {
ctx := context.Background()

a, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "BookRecommender",
Description: "An agent that can recommend books",
Instruction: `You are an expert book recommender. Based on the user's request, use the "search_book" tool to find relevant books. Finally, present the results to the user.`,
Model: NewChatModel(),
ToolsConfig: adk.ToolsConfig{
ToolsNodeConfig: compose.ToolsNodeConfig{
Tools: []tool.BaseTool{NewBookRecommender(), NewAskForClarificationTool()},
},
},
})
if err != nil {
log.Fatal(fmt.Errorf("failed to create chatmodel: %w", err))
}

return a
}

internal/checkpoint.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package internal

import "context"

func NewInMemoryStore() *InMemoryStore {
return &InMemoryStore{m: make(map[string][]byte)}
}

type InMemoryStore struct {
m map[string][]byte
}

func (i *InMemoryStore) Get(ctx context.Context, checkPointID string) ([]byte, bool, error) {
data, ok := i.m[checkPointID]
return data, ok, nil
}

func (i *InMemoryStore) Set(ctx context.Context, checkPointID string, checkPoint []byte) error {
i.m[checkPointID] = checkPoint
return nil
}

internal/toole.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package internal

import (
"context"
"log"

"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/components/tool/utils"
"github.com/cloudwego/eino/compose"
)

type askForClarificationOptions struct {
NewInput *string
}

func WithNewInput(input string) tool.Option {
return tool.WrapImplSpecificOptFn(func(t *askForClarificationOptions) {
t.NewInput = &input
})
}

type AskForClarificationInput struct {
Question string `json:"question" jsonschema:"description=The specific question you want to ask the user to get the missing information"`
}

func NewAskForClarificationTool() tool.InvokableTool {
t, err := utils.InferOptionableTool(
"ask_for_clarification",
"Call this tool when the user's request is ambiguous or lacks the necessary information to proceed. Use it to ask a follow-up question to get the details you need, such as the book's genre, before you can use other tools effectively.",
func(ctx context.Context, input *AskForClarificationInput, opts ...tool.Option) (output string, err error) {
o := tool.GetImplSpecificOptions[askForClarificationOptions](nil, opts...)
if o.NewInput == nil {
return "", compose.NewInterruptAndRerunErr(input.Question)
}
return *o.NewInput, nil
})
if err != nil {
log.Fatal(err)
}
return t
}

type BookSearchInput struct {
Genre string `json:"genre" jsonschema:"description=Preferred book genre,enum=fiction,enum=sci-fi,enum=mystery,enum=biography,enum=business"`
MaxPages int `json:"max_pages" jsonschema:"description=Maximum page length (0 for no limit)"`
MinRating int `json:"min_rating" jsonschema:"description=Minimum user rating (0-5 scale)"`
}

type BookSearchOutput struct {
Books []string
}

func NewBookRecommender() tool.InvokableTool {
bookSearchTool, err := utils.InferTool("search_book", "Search books based on user preferences", func(ctx context.Context, input *BookSearchInput) (output *BookSearchOutput, err error) {
// search code
// ...
return &BookSearchOutput{Books: []string{"God's blessing on this wonderful world!"}}, nil
})
if err != nil {
log.Fatalf("failed to create search book tool: %v", err)
}
return bookSearchTool
}

internal/util.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/*
* Copyright 2025 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package internal

import (
"encoding/json"
"fmt"
"io"
"log"
"strings"

"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/schema"
)

func Event(event *adk.AgentEvent) {
fmt.Printf("name: %s\npath: %s", event.AgentName, event.RunPath)
if event.Output != nil && event.Output.MessageOutput != nil {
if m := event.Output.MessageOutput.Message; m != nil {
if len(m.Content) > 0 {
if m.Role == schema.Tool {
fmt.Printf("\ntool response: %s", m.Content)
} else {
fmt.Printf("\nanswer: %s", m.Content)
}
}
if len(m.ToolCalls) > 0 {
for _, tc := range m.ToolCalls {
fmt.Printf("\ntool name: %s", tc.Function.Name)
fmt.Printf("\narguments: %s", tc.Function.Arguments)
}
}
} else if s := event.Output.MessageOutput.MessageStream; s != nil {
toolMap := map[int][]*schema.Message{}
var contentStart bool
charNumOfOneRow := 0
maxCharNumOfOneRow := 120
for {
chunk, err := s.Recv()
if err != nil {
if err == io.EOF {
break
}
fmt.Printf("error: %v", err)
return
}
if chunk.Content != "" {
if !contentStart {
contentStart = true
if chunk.Role == schema.Tool {
fmt.Printf("\ntool response: ")
} else {
fmt.Printf("\nanswer: ")
}
}

charNumOfOneRow += len(chunk.Content)
if strings.Contains(chunk.Content, "\n") {
charNumOfOneRow = 0
} else if charNumOfOneRow >= maxCharNumOfOneRow {
fmt.Printf("\n")
charNumOfOneRow = 0
}
fmt.Printf(chunk.Content)
}

if len(chunk.ToolCalls) > 0 {
for _, tc := range chunk.ToolCalls {
index := tc.Index
if index == nil {
fmt.Errorf("index is nil")
}
toolMap[*index] = append(toolMap[*index], &schema.Message{
Role: chunk.Role,
ToolCalls: []schema.ToolCall{
{
ID: tc.ID,
Type: tc.Type,
Index: tc.Index,
Function: schema.FunctionCall{
Name: tc.Function.Name,
Arguments: tc.Function.Arguments,
},
},
},
})
}
}
}

for _, msgs := range toolMap {
m, err := schema.ConcatMessages(msgs)
if err != nil {
log.Fatalf("ConcatMessage failed: %v", err)
return
}
fmt.Printf("\ntool name: %s", m.ToolCalls[0].Function.Name)
fmt.Printf("\narguments: %s", m.ToolCalls[0].Function.Arguments)
}
}
}
if event.Action != nil {
if event.Action.TransferToAgent != nil {
fmt.Printf("\naction: transfer to %v", event.Action.TransferToAgent.DestAgentName)
}
if event.Action.Interrupted != nil {
ii, _ := json.MarshalIndent(event.Action.Interrupted.Data, " ", " ")
fmt.Printf("\naction: interrupted")
fmt.Printf("\ninterrupt snapshot: %v", string(ii))
}
if event.Action.Exit {
fmt.Printf("\naction: exit")
}
}
if event.Err != nil {
fmt.Printf("\nerror: %v", event.Err)
}
fmt.Println()
fmt.Println()
}

runner.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package main

import (
"bufio"
"context"
"demo/chat_model_agent/internal"
"fmt"
"log"
"os"

"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/components/tool"
)

func main() {
ctx := context.Background()
a := internal.NewBookRecommendAgent()
runner := adk.NewRunner(ctx, adk.RunnerConfig{
EnableStreaming: true, // you can disable streaming here
Agent: a,
CheckPointStore: internal.NewInMemoryStore(),
})
iter := runner.Query(ctx, "recommend a book to me", adk.WithCheckPointID("1"))
for {
event, ok := iter.Next()
if !ok {
break
}
if event.Err != nil {
log.Fatal(event.Err)
}

internal.Event(event)
}

scanner := bufio.NewScanner(os.Stdin)
fmt.Print("new input is:\n")
scanner.Scan()
nInput := scanner.Text()

iter, err := runner.Resume(ctx, "1", adk.WithToolOptions([]tool.Option{internal.WithNewInput(nInput)}))
if err != nil {
log.Fatal(err)
}
for {
event, ok := iter.Next()
if !ok {
break
}
if event.Err != nil {
log.Fatal(event.Err)
}

internal.Event(event)
}
}

参考

https://www.cloudwego.io/zh/docs/eino/core_modules/eino_adk/agent_implementation/chat_model_agent/