Skip to content

Commit e089332

Browse files
feat(eino): add quick start to human-in-the-loop doc (#1466)
1 parent d90867d commit e089332

File tree

1 file changed

+198
-0
lines changed

1 file changed

+198
-0
lines changed

content/zh/docs/eino/core_modules/eino_adk/agent_hitl.md

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,204 @@ weight: 8
2222
2. 帮助最终用户尽可能轻松地回答上述问题。
2323
3. 使框架能够自动并开箱即用地回答上述问题。
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

Comments
 (0)