Skip to content

Commit 4234d8b

Browse files
feat: update eino hitl doc (#1460)
1 parent 6c4e0bc commit 4234d8b

File tree

3 files changed

+112
-60
lines changed

3 files changed

+112
-60
lines changed

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

Lines changed: 112 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -611,104 +611,156 @@ human-in-the-loop框架保持与现有代码的完全向后兼容性。所有先
611611

612612
### 1. 图中断兼容性
613613

614-
在节点/工具中使用 `NewInterruptAndRerunErr` 的先前图中断流程继续保持不变:
614+
在节点/工具中使用已弃用的 `NewInterruptAndRerunErr``InterruptAndRerun` 的先前图中断流程将继续被支持,但需要一个关键的额外步骤:**错误包装**
615+
616+
由于这些遗留函数不是地址感知的,调用它们的组件有责任捕获错误,并使用 `WrapInterruptAndRerunIfNeeded` 辅助函数将地址信息包装进去。这通常在协调遗留组件的复合节点内部完成。
617+
618+
> **注意**:如果您选择****使用 `WrapInterruptAndRerunIfNeeded`,遗留行为将被保留。最终用户仍然可以像以前一样使用 `ExtractInterruptInfo` 从错误中获取信息。但是,由于产生的中断上下文将缺少正确的地址,因此将无法对该特定中断点使用新的定向恢复 API。要完全启用新的地址感知功能,必须进行包装。
615619
616620
```go
617-
// 先前的方法仍然有效
618-
func myTool(ctx context.Context, input string) (string, error) {
621+
// 1. 一个使用已弃用中断的遗留工具
622+
func myLegacyTool(ctx context.Context, input string) (string, error) {
619623
// ... tool 逻辑
624+
// 这个错误不是地址感知的。
620625
return "", compose.NewInterruptAndRerunErr("需要用户批准")
621626
}
622627

623-
// 最终用户代码保持不变
628+
// 2. 一个调用遗留工具的复合节点
629+
var legacyToolNode = compose.InvokableLambda(func(ctx context.Context, input string) (string, error) {
630+
out, err := myLegacyTool(ctx, input)
631+
if err != nil {
632+
// 关键:调用者必须包装错误以添加地址。
633+
// "tool:legacy_tool" 段将被附加到当前地址。
634+
segment := compose.AddressSegment{Type: "tool", ID: "legacy_tool"}
635+
return "", compose.WrapInterruptAndRerunIfNeeded(ctx, segment, err)
636+
}
637+
return out, nil
638+
})
639+
640+
// 3. 最终用户代码现在可以看到完整地址。
624641
_, err := graph.Invoke(ctx, input)
625642
if err != nil {
626643
interruptInfo, exists := compose.ExtractInterruptInfo(err)
627644
if exists {
628-
// 现在你可以获得增强的信息
629-
fmt.Printf("中断上下文: %+v\n", interruptInfo.InterruptContexts)
630-
// 先前的字段仍然可用
631-
fmt.Printf("Before nodes: %v\n", interruptInfo.BeforeNodes)
645+
// 中断上下文现在将拥有一个正确的、完全限定的地址。
646+
fmt.Printf("Interrupt Address: %s\n", interruptInfo.InterruptContexts[0].Address.String())
632647
}
633648
}
634649
```
635650

636-
**增强功能**`InterruptInfo` 现在包含一个额外的 `[]*InterruptCtx` 字段,提供对中断链的结构化访问,同时保留所有现有功能。
651+
**增强功能**:通过包装错误,`InterruptInfo` 将包含一个正确的 `[]*InterruptCtx`,其中包含完全限定的地址,从而允许遗留组件无缝集成到新的人机协同框架中。
652+
653+
### 2. 对编译时静态中断图的兼容性
654+
655+
通过 `WithInterruptBeforeNodes``WithInterruptAfterNodes` 添加的先前静态中断图继续有效,但状态处理的方式得到了显著改进。
656+
657+
当静态中断被触发时,会生成一个 `InterruptCtx`,其地址指向图本身。关键在于,`InterruptCtx.Info` 字段现在直接暴露了该图的状态。
637658

638-
### 2. 静态图中断兼容性
659+
这启用了一个更直接、更直观的工作流:
660+
1. 最终用户收到 `InterruptCtx`,并可以通过 `.Info` 字段检查图的实时状态。
661+
2. 他们可以直接修改这个状态对象。
662+
3. 然后,他们可以通过 `ResumeWithData``InterruptCtx.ID` 将修改后的状态对象传回以恢复执行。
639663

640-
通过 `WithInterruptBeforeNodes``WithInterruptAfterNodes` 添加到图上的先前静态中断继续工作:
664+
这种新模式通常不再需要使用旧的 `WithStateModifier` 选项,尽管为了完全的向后兼容性,该选项仍然可用。
641665

642666
```go
643-
// 先前的静态中断配置仍然有效
644-
graph, err := myGraph.Compile(ctx,
645-
compose.WithInterruptBeforeNodes([]string{"critical_node"}),
646-
compose.WithInterruptAfterNodes([]string{"validation_node"})
647-
)
667+
// 1. 定义一个拥有自己本地状态的图
668+
type MyGraphState struct {
669+
SomeValue string
670+
}
648671

649-
// 当中断时,最终用户获得增强的信息
650-
interruptInfo, exists := compose.ExtractInterruptInfo(err)
651-
if exists {
652-
// 先前的字段仍然可用
653-
fmt.Printf("Before nodes: %v\n", interruptInfo.BeforeNodes)
654-
fmt.Printf("After nodes: %v\n", interruptInfo.AfterNodes)
655-
656-
// 新的增强功能:访问结构化的中断上下文
657-
if len(interruptInfo.InterruptContexts) > 0 {
658-
interruptCtx := interruptInfo.InterruptContexts[0]
659-
fmt.Printf("图状态: %+v\n", interruptCtx.Info)
660-
661-
// 最终用户可以直接修改状态并将其传回
662-
// 不再需要使用 WithStateModifier(尽管它仍然有效)
672+
g := compose.NewGraph[string, string](compose.WithGenLocalState(func(ctx context.Context) *MyGraphState {
673+
return &MyGraphState{SomeValue: "initial"}
674+
}))
675+
// ... 向图中添加节点1和节点2 ...
676+
677+
// 2. 使用静态中断点编译图
678+
// 这将在 "node_1" 节点完成后中断图本身。
679+
graph, err := g.Compile(ctx, compose.WithInterruptAfterNodes([]string{"node_1"}))
680+
681+
// 3. 运行图,这将触发静态中断
682+
_, err = graph.Invoke(ctx, "start")
683+
684+
// 4. 提取中断上下文和图的状态
685+
interruptInfo, isInterrupt := compose.ExtractInterruptInfo(err)
686+
if isInterrupt {
687+
interruptCtx := interruptInfo.InterruptContexts[0]
688+
689+
// .Info 字段暴露了图的当前状态
690+
graphState, ok := interruptCtx.Info.(*MyGraphState)
691+
if ok {
692+
// 5. 直接修改状态
693+
fmt.Printf("Original state value: %s\n", graphState.SomeValue) // 打印 "initial"
694+
graphState.SomeValue = "a-new-value-from-user"
695+
696+
// 6. 通过传回修改后的状态对象来恢复
697+
resumeCtx := compose.ResumeWithData(context.Background(), interruptCtx.ID, graphState)
698+
result, err := graph.Invoke(resumeCtx, "start")
699+
// ... 执行将继续,并且 node_2 现在将看到修改后的状态。
663700
}
664701
}
665702
```
666703

667-
**增强功能**:除了 `[]BeforeNodes``[]AfterNodes`,现在还返回一个 `InterruptCtx`,使最终用户可以直接访问图状态。对于喜欢先前方法的开发者,`WithStateModifier` 选项仍然可用。
668-
669704
### 3. Agent 中断兼容性
670705

671-
先前的 agent 中断流程继续保持不变:
706+
与旧版 agent 的兼容性是在数据结构层面维护的,确保了旧的 agent 实现能在新框架内继续运作。其关键在于 `adk.InterruptInfo``adk.ResumeInfo` 结构体是如何被填充的。
707+
708+
**对最终用户(应用层)而言:**
709+
当从 agent 收到一个中断时,`adk.InterruptInfo` 结构体中会同时填充以下两者:
710+
- 新的、结构化的 `InterruptContexts` 字段。
711+
- 遗留的 `Data` 字段,它将包含原始的中断信息(例如 `ChatModelAgentInterruptInfo``WorkflowInterruptInfo`)。
712+
713+
这使得最终用户可以逐步迁移他们的应用逻辑来使用更丰富的 `InterruptContexts`,同时在需要时仍然可以访问旧的 `Data` 字段。
714+
715+
**对 Agent 开发者而言:**
716+
当一个旧版 agent 的 `Resume` 方法被调用时,它收到的 `adk.ResumeInfo` 结构体仍然包含现已弃用的嵌入式 `InterruptInfo` 字段。该字段被填充了相同的遗留数据结构,允许 agent 开发者维持其现有的恢复逻辑,而无需立即更新到新的地址感知 API。
672717

673718
```go
674-
// Agent 中断模式保持不变
675-
func (a *myAgent) Run(ctx context.Context, input *adk.AgentInput) *adk.AsyncIterator[*adk.AgentEvent] {
676-
// ... agent 逻辑
677-
return adk.Interrupt(ctx, "需要用户输入")
719+
// --- 最终用户视角 ---
720+
721+
// 在 agent 运行后,你收到了一个中断事件。
722+
if event.Action != nil && event.Action.Interrupted != nil {
723+
interruptInfo := event.Action.Interrupted
724+
725+
// 1. 新方式:访问结构化的中断上下文
726+
if len(interruptInfo.InterruptContexts) > 0 {
727+
fmt.Printf("New structured context available: %+v\n", interruptInfo.InterruptContexts[0])
728+
}
729+
730+
// 2. 旧方式(仍然有效):访问遗留的 Data 字段
731+
if chatInterrupt, ok := interruptInfo.Data.(*adk.ChatModelAgentInterruptInfo); ok {
732+
fmt.Printf("Legacy ChatModelAgentInterruptInfo still accessible.\n")
733+
// ... 使用旧结构体的逻辑
734+
}
678735
}
679736

680-
// 最终用户访问模式保持不变
681-
func (a *myAgent) Resume(ctx context.Context, info *adk.ResumeInfo) *adk.AsyncIterator[*adk.AgentEvent] {
682-
// 先前的信息仍然可用
683-
if info.WasInterrupted {
684-
fmt.Printf("中断数据: %v\n", info.InterruptInfo.Data)
685-
// 增强:现在还可以访问结构化的中断上下文
686-
if info.InterruptInfo != nil && len(info.InterruptInfo.InterruptContexts) > 0 {
687-
fmt.Printf("中断上下文: %+v\n", info.InterruptInfo.InterruptContexts[0])
737+
738+
// --- Agent 开发者视角 ---
739+
740+
// 在一个旧版 agent 的 Resume 方法内部:
741+
func (a *myLegacyAgent) Resume(ctx context.Context, info *adk.ResumeInfo) *adk.AsyncIterator[*adk.AgentEvent] {
742+
// 已弃用的嵌入式 InterruptInfo 字段仍然会被填充。
743+
// 这使得旧的恢复逻辑可以继续工作。
744+
if info.InterruptInfo != nil {
745+
if chatInterrupt, ok := info.InterruptInfo.Data.(*adk.ChatModelAgentInterruptInfo); ok {
746+
// ... 依赖于旧的 ChatModelAgentInterruptInfo 结构体的现有恢复逻辑
747+
fmt.Println("Resuming based on legacy InterruptInfo.Data field.")
688748
}
689749
}
690750

691-
// 恢复逻辑:继续正常执行或处理恢复数据
692-
if info.IsResumeTarget && info.ResumeData != nil {
693-
// 处理恢复数据并继续
694-
return a.handleResumeWithData(ctx, info.ResumeData)
695-
}
696-
697-
// 继续正常执行
698-
return a.Run(ctx, input)
751+
// ... 继续执行
752+
return a.Run(ctx, &adk.AgentInput{Input: "resumed execution"})
699753
}
700754
```
701755

702-
**增强功能**:来自 `AgentEvent``InterruptInfo` 包含所有先前的信息,最终用户仍然可以在 `ResumeInfo` 中访问此信息,并额外受益于结构化的中断上下文。
703-
704756
### 迁移优势
705757

706-
- **无重大变更**:现有代码无需修改即可继续工作
707-
- **渐进式采用**:团队可以按照自己的节奏采用新功能
708-
- **增强的功能**:新的寻址系统在保留现有 API 的同时提供了更丰富的上下文
709-
- **灵活的状态管理**:最终用户可以选择直接状态修改(新)或 `WithStateModifier`(现有)
758+
- **保留遗留行为**: 现有代码将继续按其原有方式运行。旧的中断模式不会导致程序崩溃,但它们也不会在不经修改的情况下自动获得新的地址感知能力
759+
- **渐进式采用**: 团队可以根据具体情况选择性地启用新功能。例如,你可以只在你需要定向恢复的工作流中,用 `WrapInterruptAndRerunIfNeeded` 来包装遗留的中断
760+
- **增强的功能**: 新的寻址系统为所有中断提供了更丰富的结构化上下文 (`InterruptCtx`),同时旧的数据字段仍然会被填充以实现完全兼容
761+
- **灵活的状态管理**: 对于静态图中断,你可以选择通过 `.Info` 字段进行现代、直接的状态修改,或者继续使用旧的 `WithStateModifier` 选项
710762

711-
这种向后兼容性确保了现有用户的平滑过渡,同时为human-in-the-loop交互提供了强大的新功能
763+
这种向后兼容性模型确保了现有用户的平滑过渡,同时为采用强大的新的 human-in-the-loop 交互功能提供了清晰的路径
712764

713765
## 实现示例
714766

1.08 KB
Loading

static/img/eino/hitl_sequence.png

178 Bytes
Loading

0 commit comments

Comments
 (0)