internal_runtime_and_mocks 模块深度解析
模块概述
internal_runtime_and_mocks 模块是整个框架的基础设施层,它提供了两个核心能力:
- 运行时支撑系统:为上层组件提供回调机制、中断管理、地址追踪和序列化等底层功能
- 测试模拟框架:为所有核心组件提供统一的 Mock 实现,使测试可以在不依赖外部资源的情况下进行
想象一下,如果把整个框架比作一个城市,那么这个模块就是城市的地下基础设施——它包括供电系统(序列化)、交通监控(回调)、紧急响应机制(中断)以及城市规划图(地址系统)。虽然这些设施在地面上不可见,但没有它们,整个城市就无法正常运转。
架构概览
(runtime_callbacks_and_execution_context)"] Interrupt["中断与寻址
(interrupt_and_addressing_runtime_primitives)"] Serialization["序列化引擎
(internal_serialization_engine)"] Safe["安全原语
(panic_safety_primitive)"] end subgraph "测试模拟层" AgentMocks["Agent 模拟
(adk_agent_contract_mocks)"] DocMocks["文档组件模拟
(document_component_mocks)"] ModelMocks["模型检索索引模拟
(model_retrieval_and_indexing_mocks)"] end Callbacks --> Interrupt Serialization --> Interrupt Interrupt --> ModelMocks Callbacks --> AgentMocks
这个模块的架构清晰地分为两层:
-
运行时核心层:提供实际运行时所需的基础设施
- 回调系统:用于组件执行的追踪和监控
- 中断与寻址:处理执行中断、恢复和执行位置追踪
- 序列化引擎:处理复杂数据结构的序列化和反序列化
- 安全原语:提供 panic 捕获和处理机制
-
测试模拟层:为所有核心组件提供 Mock 实现
- Agent 模拟:模拟 Agent 接口
- 文档组件模拟:模拟文档加载和转换
- 模型检索索引模拟:模拟模型、嵌入、索引和检索器
核心设计理念
1. 上下文驱动的执行模型
整个模块的设计围绕着 Go 的 context.Context 展开。几乎所有的运行时状态都通过 context 传递,而不是通过全局变量或显式参数。这种设计有几个关键优势:
- 可组合性:组件可以自由嵌套,状态会自动传递
- 可测试性:可以轻松创建带有特定状态的 context 进行测试
- 线程安全:context 是不可变的,避免了并发问题
例如,Address 系统就是通过 context 传递的,每个组件在执行时都会将自己的地址段追加到 context 中,形成一个完整的执行路径。
2. 类型安全的序列化
序列化引擎采用了一种独特的设计:它不是简单地使用 JSON 序列化,而是先将数据转换为一个内部结构(internalStruct),然后再序列化为 JSON。这种设计使得:
- 可以处理任意复杂的类型嵌套
- 保留了类型信息,反序列化时可以正确恢复类型
- 支持自定义的 Marshaler/Unmarshaler
3. 中断作为第一类公民
中断系统不是简单的错误处理机制,而是一个完整的执行状态管理系统。它可以:
- 捕获完整的执行上下文(地址、状态、信息)
- 支持中断的嵌套和传播
- 提供精确的恢复机制,可以从任意中断点继续执行
关键设计决策
决策 1:使用 context 传递所有运行时状态
选择:所有运行时状态(回调管理器、地址、中断状态等)都通过 context 传递,而不是使用全局变量或显式参数。
原因:
- 这种设计与 Go 语言的惯用法一致
- 提供了自然的作用域管理——当 context 被取消时,相关状态也会被清理
- 使得组件可以自由嵌套,无需手动传递状态
权衡:
- ✅ 优点:高可组合性、易于测试、线程安全
- ❌ 缺点:类型安全减弱(需要类型断言)、调试难度增加(状态不直观)
决策 2:自定义序列化引擎而不是使用标准库
选择:实现了一个完整的自定义序列化引擎,而不是直接使用 encoding/json。
原因:
- 需要处理 interface{} 类型的序列化和反序列化
- 需要保留完整的类型信息,包括指针层级
- 需要支持自定义的 Marshaler/Unmarshaler
权衡:
- ✅ 优点:功能强大、灵活、类型安全
- ❌ 缺点:代码复杂度高、性能可能不如标准库、需要预先注册类型
决策 3:中断信号使用树形结构
选择:中断信号(InterruptSignal)采用树形结构,可以包含子中断信号。
原因:
- 支持复杂的嵌套执行场景(如一个 Agent 调用另一个 Agent)
- 可以精确地表示中断的根源和传播路径
- 便于实现精确的恢复机制
权衡:
- ✅ 优点:表达能力强、支持复杂场景
- ❌ 缺点:实现复杂、使用难度较高
子模块概览
runtime_callbacks_and_execution_context
这个子模块提供了回调系统的核心实现,包括回调接口、回调管理器和运行信息。它就像是一个执行监控系统,可以在组件执行的各个阶段(开始、结束、错误)插入自定义逻辑。
详细信息请参考 runtime_callbacks_and_execution_context 文档。
interrupt_and_addressing_runtime_primitives
这个子模块是整个运行时的核心,它提供了中断管理、地址追踪和恢复机制。它就像是一个执行导航系统,可以精确地定位执行位置,处理中断,并支持从任意点恢复执行。
详细信息请参考 interrupt_and_addressing_runtime_primitives 文档。
internal_serialization_engine
这个子模块提供了类型安全的序列化和反序列化功能。它就像是一个数据翻译官,可以在 Go 的复杂数据结构和 JSON 之间进行精确转换,同时保留完整的类型信息。
详细信息请参考 internal_serialization_engine 文档。
adk_agent_contract_mocks
这个子模块提供了 Agent 接口的 Mock 实现,用于测试。它就像是一个演员替身,可以在测试中代替真实的 Agent,按照预设的行为执行。
详细信息请参考 adk_agent_contract_mocks 文档。
document_component_mocks
这个子模块提供了文档组件(Loader、Transformer)的 Mock 实现。它可以模拟文档的加载和转换,使测试不依赖真实的文档源。
详细信息请参考 document_component_mocks 文档。
model_retrieval_and_indexing_mocks
这个子模块提供了模型、嵌入、索引和检索器的 Mock 实现。它可以模拟这些组件的行为,使测试可以在不依赖外部服务的情况下进行。
详细信息请参考 model_retrieval_and_indexing_mocks 文档。
跨模块依赖
这个模块是整个框架的基础设施,因此它被几乎所有其他模块依赖:
- 被依赖:
adk_runtime、compose_graph_engine、flow_agents_and_retrieval等模块都依赖这个模块提供的运行时功能 - 依赖:这个模块几乎不依赖其他模块,只依赖一些基础的类型定义(如
schema)
这种依赖关系体现了它的基础设施定位——它为上层模块提供服务,而不依赖上层模块。
新贡献者指南
常见陷阱
-
忘记注册类型:使用序列化引擎时,必须先通过
GenericRegister注册自定义类型,否则序列化会失败。 -
context 传递:所有运行时状态都通过 context 传递,因此必须确保 context 在组件调用链中正确传递,不要创建新的 context 而不继承父 context。
-
中断处理:中断信号是 error 类型,但它不是普通的错误——它包含完整的执行上下文。处理中断时,不要简单地将其视为错误,而应该使用专门的 API 处理。
-
Mock 使用:使用 Mock 时,必须通过
EXPECT()方法设置期望行为,否则 Mock 会返回零值。
扩展点
-
自定义回调:可以通过实现
Handler接口创建自定义回调,并通过GlobalHandlers注册全局回调。 -
自定义序列化:对于特殊类型,可以实现
json.Marshaler和json.Unmarshaler接口,序列化引擎会自动使用这些自定义实现。 -
新的 Mock:当添加新的组件接口时,可以使用
mockgen工具生成相应的 Mock 实现。
调试技巧
-
打印地址:使用
GetCurrentAddress(ctx).String()可以打印当前的执行地址,这对于调试中断和恢复非常有用。 -
检查回调:可以通过
managerFromCtx(ctx)检查当前 context 中的回调管理器。 -
序列化测试:在使用自定义类型前,先写一个简单的测试验证序列化和反序列化是否正常工作。