@@ -22,6 +22,204 @@ weight: 8
22222 . 帮助最终用户尽可能轻松地回答上述问题。
23233 . 使框架能够自动并开箱即用地回答上述问题。
2424
25+ ## 快速开始
26+
27+ 我们用一个简单的订票 Agent 来演示功能,这个 Agent 在实际完成订票前,会向用户寻求“确认”,用户可以“同意”或者“拒绝”本次订票操作。这个例子的完整代码在:[ https://github.com/cloudwego/eino-examples/tree/main/adk/human-in-the-loop/1_approval ] ( https://github.com/cloudwego/eino-examples/tree/main/adk/human-in-the-loop/1_approval )
28+
29+ 1 . 创建一个 ChatModelAgent,并配置一个用来订票的 Tool。
30+
31+ ``` go
32+ import (
33+ " context"
34+ " fmt"
35+ " log"
36+
37+ " github.com/cloudwego/eino/adk"
38+ " github.com/cloudwego/eino/components/tool"
39+ " github.com/cloudwego/eino/components/tool/utils"
40+ " github.com/cloudwego/eino/compose"
41+
42+ " github.com/cloudwego/eino-examples/adk/common/model"
43+ tool2 " github.com/cloudwego/eino-examples/adk/common/tool"
44+ )
45+
46+ func NewTicketBookingAgent () adk .Agent {
47+ ctx := context.Background ()
48+
49+ type bookInput struct {
50+ Location string ` json:"location"`
51+ PassengerName string ` json:"passenger_name"`
52+ PassengerPhoneNumber string ` json:"passenger_phone_number"`
53+ }
54+
55+ getWeather , err := utils.InferTool (
56+ " BookTicket" ,
57+ " this tool can book ticket of the specific location" ,
58+ func (ctx context.Context , input bookInput) (output string , err error ) {
59+ return " success" , nil
60+ })
61+ if err != nil {
62+ log.Fatal (err)
63+ }
64+
65+ a , err := adk.NewChatModelAgent (ctx, &adk.ChatModelAgentConfig {
66+ Name: " TicketBooker" ,
67+ Description: " An agent that can book tickets" ,
68+ Instruction: ` You are an expert ticket booker.
69+ Based on the user's request, use the "BookTicket" tool to book tickets.` ,
70+ Model: model.NewChatModel (),
71+ ToolsConfig: adk.ToolsConfig {
72+ ToolsNodeConfig: compose.ToolsNodeConfig {
73+ Tools: []tool.BaseTool {
74+ // InvokableApprovableTool 是 eino-examples 提供的一个 tool 装饰器,
75+ // 可以为任意的 InvokableTool 加上“审批中断”功能
76+ &tool2.InvokableApprovableTool {InvokableTool: getWeather},
77+ },
78+ },
79+ },
80+ })
81+ if err != nil {
82+ log.Fatal (fmt.Errorf (" failed to create chatmodel: % w" , err))
83+ }
84+
85+ return a
86+ }
87+ ```
88+
89+ 2 . 创建一个 Runner,配置 CheckPointStore,并运行,传入一个 CheckPointID。Eino 用 CheckPointStore 来保存 Agent 中断时的运行状态,这里用的 InMemoryStore,保存在内存中。实际使用中,推荐用分布式存储比如 redis。另外,Eino 用 CheckPointID 来唯一标识和串联“中断前”和“中断后”的两次(或多次)运行。
90+
91+ ``` go
92+ a := NewTicketBookingAgent ()
93+ runner := adk.NewRunner (ctx, adk.RunnerConfig {
94+ EnableStreaming : true , // you can disable streaming here
95+ Agent : a,
96+
97+ // provide a CheckPointStore for eino to persist the execution state of the agent for later resumption.
98+ // Here we use an in-memory store for simplicity.
99+ // In the real world, you can use a distributed store like Redis to persist the checkpoints.
100+ CheckPointStore : store.NewInMemoryStore (),
101+ })
102+ iter := runner.Query (ctx, " book a ticket for Martin, to Beijing, on 2025-12-01, the phone number is 1234567. directly call tool." , adk.WithCheckPointID (" 1" ))
103+ ```
104+
105+ 3 . 从 AgentEvent 中拿到 interrupt 信息 ` event.Action.Interrupted.InterruptContexts[0].Info ` ,在这里是“准备给谁订哪趟车,是否同意”。同时会拿到一个 InterruptID(` event.Action.Interrupted.InterruptContexts[0].ID ` ),Eino 框架用这个 InterruptID 来标识“哪里发生了中断”。这里直接打印在了终端上,实际使用中,可能需要作为 HTTP 响应返回给前端。
106+
107+ ``` go
108+ var lastEvent *adk.AgentEvent
109+ for {
110+ event , ok := iter.Next ()
111+ if !ok {
112+ break
113+ }
114+ if event.Err != nil {
115+ log.Fatal (event.Err )
116+ }
117+
118+ prints.Event (event)
119+
120+ lastEvent = event
121+ }
122+
123+ // this interruptID is crucial 'locator' for Eino to know where the interrupt happens,
124+ // so when resuming later, you have to provide this same `interruptID` along with the approval result back to Eino
125+ interruptID := lastEvent.Action .Interrupted .InterruptContexts [0 ].ID
126+ ```
127+
128+ 4 . 给用户展示 interrupt 信息,并接收到用户的响应,比如“同意”。在这个例子里面,都是在本地终端上展示给用户和接收用户输入的。在实际应用中,可能是用 ChatBot 做输入输出。
129+
130+ ``` go
131+ var apResult *tool.ApprovalResult
132+ for {
133+ scanner := bufio.NewScanner (os.Stdin )
134+ fmt.Print (" your input here: " )
135+ scanner.Scan ()
136+ fmt.Println ()
137+ nInput := scanner.Text ()
138+ if strings.ToUpper (nInput) == " Y" {
139+ apResult = &tool.ApprovalResult {Approved: true }
140+ break
141+ } else if strings.ToUpper (nInput) == " N" {
142+ // Prompt for reason when denying
143+ fmt.Print (" Please provide a reason for denial: " )
144+ scanner.Scan ()
145+ reason := scanner.Text ()
146+ fmt.Println ()
147+ apResult = &tool.ApprovalResult {Approved: false , DisapproveReason: &reason}
148+ break
149+ }
150+
151+ fmt.Println (" invalid input, please input Y or N" )
152+ }
153+ ```
154+
155+ 样例输出:
156+
157+ ``` json
158+ name: TicketBooker
159+ path: [{TicketBooker}]
160+ tool name: BookTicket
161+ arguments: {"location":"Beijing","passenger_name":"Martin","passenger_phone_number":"1234567"}
162+
163+ name: TicketBooker
164+ path: [{TicketBooker}]
165+ tool 'BookTicket' interrupted with arguments '{"location":"Beijing","passenger_name":"Martin","passenger_phone_number":"1234567"}', waiting for your approval, please answer with Y/N
166+
167+ your input here: Y
168+ ```
169+
170+ 5 . 调用 Runner.ResumeWithParams,传入同一个 InterruptID,以及用来恢复的数据,这里是“同意”。在这个例子里,首次 ` Runner.Query ` 和之后的 ` Runner.ResumeWithParams ` 是在一个实例中,在真实场景,可能是 ChatBot 前端的两次请求,打到服务端的两个实例中。只要 CheckPointID 两次相同,且给 Runner 配置的 CheckPointStore 是分布式存储,Eino 就能做到跨实例的中断恢复。
171+
172+ ``` go
173+ _// here we directly resumes right in the same instance where the original `Runner.Query` happened.
174+ // In the real world, the original `Runner.Run/Query` and the subsequent `Runner.ResumeWithParams`
175+ // can happen in different processes or machines, as long as you use the same `CheckPointID`,
176+ // and you provided a distributed `CheckPointStore` when creating the `Runner` instance.
177+ iter , err := runner.ResumeWithParams (ctx, " 1" , &adk.ResumeParams {
178+ Targets : map [string ]any{
179+ interruptID: apResult,
180+ },
181+ })
182+ if err != nil {
183+ log.Fatal (err)
184+ }
185+ for {
186+ event , ok := iter.Next ()
187+ if !ok {
188+ break
189+ }
190+
191+ if event.Err != nil {
192+ log.Fatal (event.Err )
193+ }
194+
195+ prints.Event (event)
196+ __}_
197+ ```
198+
199+ 完整样例输出:
200+
201+ ``` yaml
202+ name : TicketBooker
203+ path : [{TicketBooker}]
204+ tool name : BookTicket
205+ arguments : {"location":"Beijing","passenger_name":"Martin","passenger_phone_number":"1234567"}
206+
207+ name : TicketBooker
208+ path : [{TicketBooker}]
209+ tool 'BookTicket' interrupted with arguments '{"location":"Beijing","passenger_name":"Martin","passenger_phone_number":"1234567"}', waiting for your approval, please answer with Y/N
210+
211+ your input here : Y
212+
213+ name : TicketBooker
214+ path : [{TicketBooker}]
215+ tool response : success
216+
217+ name : TicketBooker
218+ path : [{TicketBooker}]
219+ answer : The ticket for Martin to Beijing on 2025-12-01 has been successfully booked. If you need any more assistance, feel free
220+ to ask!
221+ ` ` `
222+
25223## 架构概述
26224
27225以下流程图说明了高层次的中断/恢复流程:
0 commit comments