将 note、pdd、prd、roadmap 四类文档分别归入 `design/` 下对应子目录中,并新增 `.gitkeep` 占位文件
11 KiB
Phase 6 — ToolDefinition IR 正式化实施方案
背景与目标
在 agcore v0.2 路线图中,Phase 6 旨在引入 ToolDef 新类型,替换已标记 #[deprecated(since = "0.1.0")] 的 ToolDefinition(即 OpenaiToolDefinition 类型别名),消除 OpenAI wire format 对核心类型系统的泄漏,建立 Provider 无关的工具定义中间表示(IR)。
预期成果:
- 核心类型系统不再直接依赖
OpenaiToolDefinition - 所有 Provider 适配层从统一的
ToolDefIR 出发,各自转换为对应 wire format - 消除
#[allow(deprecated)]抑制点,恢复 clippy 零警告状态
当前状态分析
当前代码库中工具定义相关的关键状态如下:
-
OpenaiToolDefinition结构体定义于llm/types/tool.rs,包含 4 个字段:name: Stringdescription: Option<String>parameters: Valuestrict: Option<bool>
当前存在多处
#[allow(deprecated)]抑制点,分布在llm/cycle.rs、tools/registry.rs、tools/mcp.rs、agent/agent.rs等文件中。 -
ToolDefinition类型别名定义于llm/types/mod.rs:105,标记为#[deprecated]:#[deprecated(since = "0.1.0", note = "use OpenaiToolDefinition directly")] pub type ToolDefinition = OpenaiToolDefinition; -
MessageRequest.tools字段类型为Vec<OpenaiToolDefinition>(直接引用原始类型,而非别名)。 -
引用该类型的 4 个源文件:
llm/cycle.rs:4 个方法参数使用Vec<ToolDefinition>tools/registry.rs:definitions() -> Vec<ToolDefinition>返回类型 + struct literal 构造(含strict: None)tools/mcp.rs:list_tools() -> Vec<ToolDefinition>返回类型 + struct literal 构造(含strict: None)agent/agent.rs:fn tool_definitions() -> Vec<ToolDefinition>trait 默认实现
-
Provider 适配层:
openai.rs和anthropic.rs从MessageRequest.tools读取数据并转换为各自的 wire format。openai_compat.rs和ollama.rs委托给GenericOpenaiProvider,无需直接改动。
需求推演
决策 1:strict 字段的处理
当前所有构造路径均硬编码 strict: None(registry.rs、mcp.rs),BaseTool trait 无 strict 方法,用户 API 无法设置该值。
结论:移除。 strict 是 OpenAI 的 Structured Outputs 专属字段,不属于 Provider 无关的 IR。未来如需支持,走 MessageRequest.extra 逃生舱,在各 Provider 适配层自行消费。
决策 2:description 保持 Option<String>
Anthropic 要求 description 为必填(String),但 MCP 等来源可能缺失该字段。保持 Option,由 Anthropic 适配层以 unwrap_or_default() 兜底。
决策 3:parameters 保持 Value
所有 Provider 的 wire format 均接受 JSON Schema 格式的 Value。当前不做 typed 方案,保留 Value。
决策 4:采用直接切断而非阶段性 deprecation
v0.1 已标记 #[deprecated],用户已有预期。pre-1.0 阶段的 breaking change 是合理的。OpenaiToolDefinition 保留但降级为 #[doc(hidden)]。
方案设计
ToolDef 结构体
位置:src/llm/types/tool.rs(与 OpenaiToolDefinition 同文件,不建独立文件/模块)。
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ToolDef {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub parameters: Value,
}
实现双向 From 转换:
impl From<ToolDef> for OpenaiToolDefinition {
fn from(t: ToolDef) -> Self {
Self {
name: t.name,
description: t.description,
parameters: t.parameters,
strict: None,
}
}
}
impl From<OpenaiToolDefinition> for ToolDef {
fn from(t: OpenaiToolDefinition) -> Self {
Self {
name: t.name,
description: t.description,
parameters: t.parameters,
}
}
}
serde 属性与 OpenaiToolDefinition 原有属性一致,保证 JSON 序列化兼容。
明确不做:
- builder 模式(Rust struct literal +
..Default::default()已足够) #[non_exhaustive](IR 类型自有完整控制权,不需要)- 独立文件(7 行 struct 无需独立模块)
4 单元切割计划
每步设计为可编译的安全 checkpoint。
单元 6.1 — 新增 ToolDef + From 实现
| 项目 | 内容 |
|---|---|
| 涉及文件 | llm/types/tool.rs |
| 变更内容 | 新增 ToolDef struct(约 7 行)、2 个 From impl(约 12 行) |
| 验证标准 | cargo build 编译通过(旧代码照常编译,零影响) |
| 检查点 | 新类型存在但未被消费,安全 checkpoint |
单元 6.2 — 别名切换 + 构造同步修复
| 项目 | 内容 |
|---|---|
| 涉及文件 | llm/types/mod.rs、llm/types/request_v2.rs、llm/types/request.rs、tools/registry.rs、tools/mcp.rs |
| 变更内容 | 切换别名 pub type ToolDefinition = ToolDef;MessageRequest.tools 改为 Vec<ToolDef>;registry/mcp 构造去掉 strict: None |
| 验证标准 | cargo build 编译通过 |
| 风险提示 | cycle.rs 方法参数使用别名,自动生效无需修改;agent/agent.rs trait 默认实现使用别名,自动适配;暂不移除 #[allow(deprecated)] |
单元 6.3 — Provider 适配
| 项目 | 内容 |
|---|---|
| 涉及文件 | llm/provider/openai.rs |
| 变更内容 | convert_request() 中 `tool_defs.into_iter().map( |
| 验证标准 | cargo test --all-targets 全部通过 |
| 不修改的文件 | anthropic.rs(同名字段访问自动适配)、openai_compat.rs/ollama.rs(委托给 GenericOpenaiProvider) |
单元 6.4 — 清理
| 项目 | 内容 |
|---|---|
| 涉及文件 | llm/cycle.rs、tools/registry.rs、tools/mcp.rs、agent/agent.rs、llm/types/mod.rs、llm/types/tool.rs |
| 变更内容 | 移除所有与 ToolDefinition 相关的 #[allow(deprecated)];llm/types/mod.rs 移除旧 #[deprecated] 别名(仅保留 pub use tool::ToolDef);OpenaiToolDefinition 降级为 #[doc(hidden)]。精确列表由 cargo clippy -D warnings 检出——clippy 会标记所有不再需要的 #[allow] |
| 验证标准 | cargo clippy --all-targets -- -D warnings 零警告;cargo test --all-targets 全部通过 |
| 无需改动 | 测试代码(无一直接引用 ToolDefinition) |
Provider 适配策略
Provider 适配层改动最小化,仅在序列化入口处加一层 From 转换:
| Provider | 适配方式 | 改动 |
|---|---|---|
OpenAI(GenericOpenaiProvider) |
MessageRequest.tools: Vec<ToolDef> → lambda 内改为 .map(|t| OpenaiTool::Function { function: t.into() }),将 ToolDef 通过 Into 转为 OpenaiToolDefinition |
convert_request lambda 内 +.into() |
| Anthropic | t.name / t.description / t.parameters 字段名不变,直接访问 |
零改动 |
| OpenAI Compat(DeepSeek、Qwen) | 委托给 GenericOpenaiProvider |
零改动 |
| Ollama | 委托给 GenericOpenaiProvider |
零改动 |
变更清单汇总
| 文件 | 改动类型 | 估计行数 |
|---|---|---|
llm/types/tool.rs |
+ToolDef + 2x From |
+19 |
llm/types/mod.rs |
改别名 + re-export | ~3 |
llm/types/request_v2.rs |
改 tools 字段 + import |
~2 |
llm/cycle.rs |
移 #[allow(deprecated)] |
-1 |
tools/registry.rs |
构造去掉 strict + 移 allow |
~4 |
tools/mcp.rs |
构造去掉 strict + 移 allow |
~4 |
agent/agent.rs |
移 #[allow(deprecated)] |
-1 |
llm/provider/openai.rs |
convert_request 加 .map(Into::into) |
+2 |
| 新增 roundtrip 测试 | MessageRequest 序列化 roundtrip 验证 |
+15 |
| 合计 | 约 47 行(+ 约 15 行测试) |
测试策略
新增一条 MessageRequest 序列化 roundtrip 测试,覆盖 ToolDef 的 JSON 序列化/反序列化兼容性。该测试验证 ToolDef 的 serde 属性与 OpenaiToolDefinition 一致,确保 wire format 兼容。
实施步骤
- 新建分支
phase-6-tooldef-ir - 按单元 6.1 → 6.2 → 6.3 → 6.4 顺序执行,每步提交一个 commit
- 每步执行对应的验证标准
- 全量通过后创建 PR
git checkout -b phase-6-tooldef-ir
# 执行单元 6.1 → commit
# 执行单元 6.2 → commit
# 执行单元 6.3 → commit
# 执行单元 6.4 → commit
# 全量验证
用户迁移指引
Phase 6 涉及公共 API 类型替换,下游用户升级到 v0.2 时需注意:
| 旧用法 | 新用法 |
|---|---|
use agcore::llm::types::ToolDefinition |
use agcore::llm::types::ToolDef(别名已移除) |
use agcore::llm::types::OpenaiToolDefinition |
use agcore::llm::types::ToolDef(OpenaiToolDefinition 已降级为 #[doc(hidden)]) |
直接构造 ToolDefinition { strict: None, .. } |
构造 ToolDef { .. }(去掉 strict 字段) |
OpenaiToolDefinition 仍保留但标记 #[doc(hidden)],极端情况仍需使用时可通过全路径访问。
验证标准
| 阶段 | 验证命令 |
|---|---|
| 单元 6.1 | cargo build 编译通过 |
| 单元 6.2 | cargo build 编译通过(新旧代码全量编译) |
| 单元 6.3 | cargo test --all-targets 全部通过 |
| 单元 6.4 | cargo clippy --all-targets -- -D warnings 零警告;cargo test --all-targets 全部通过 |
| 最终 | cargo build --all-targets + cargo test --all-targets + cargo clippy --all-targets -- -D warnings 全绿 |
风险与缓解
| 风险 | 说明 | 缓解措施 |
|---|---|---|
| Struct literal 断层 | Step 6.2 切别名与构造修复若不同步,registry/mcp 中使用 OpenaiToolDefinition struct literal 的构造代码会编译失败 |
别名切换与构造修复合并在同一单元,原子化提交 |
遗漏 #[allow(deprecated)] |
部分抑制点因 grep 遗漏而未在 6.4 移除 | clippy -D warnings 可检出;6.4 前做一次全库 grep 确认无遗漏 |
| JSON 兼容性 | ToolDef serde 属性与 OpenaiToolDefinition 不一致导致 wire format 变化 |
ToolDef serde 属性与 OpenaiToolDefinition 保持一致;roundtrip 测试验证 |
| Provider 适配遗漏 | 部分 Provider 分支未经测试覆盖 | cargo test --all-targets 包含 Provider 测试 |
否决记录
| 否决方案 | 原因 |
|---|---|
保留 strict 字段 |
OpenAI 专属字段,当前所有构造路径传 None。不属于 Provider 无关的 IR。未来支持走 MessageRequest.extra 逃生舱 |
| 逐步 deprecation 过渡 | pre-1.0 阶段 breaking change 合理,v0.1 已标记 deprecation,用户已有预期 |
ToolDef 建独立文件 |
约 7 行的 struct 不需要独立文件,与 OpenaiToolDefinition 共享 types/tool.rs 即可 |
ToolDef 放在 tools/ 模块 |
会创造 llm → tools 的逆向依赖,破坏模块分层 |
| 添加 builder 模式 | Rust struct literal + ..Default::default() 已足够覆盖使用场景 |
添加 #[non_exhaustive] |
IR 类型自有完整控制权,不需要对外隐藏字段 |
同 Phase 净化 parameters 类型化 |
属于独立工作,留给 v0.3+ 阶段处理 |