# Phase 6 — ToolDefinition IR 正式化实施方案 ## 背景与目标 在 agcore v0.2 路线图中,Phase 6 旨在引入 `ToolDef` 新类型,替换已标记 `#[deprecated(since = "0.1.0")]` 的 `ToolDefinition`(即 `OpenaiToolDefinition` 类型别名),消除 OpenAI wire format 对核心类型系统的泄漏,建立 Provider 无关的工具定义中间表示(IR)。 预期成果: - 核心类型系统不再直接依赖 `OpenaiToolDefinition` - 所有 Provider 适配层从统一的 `ToolDef` IR 出发,各自转换为对应 wire format - 消除 `#[allow(deprecated)]` 抑制点,恢复 clippy 零警告状态 ## 当前状态分析 当前代码库中工具定义相关的关键状态如下: 1. **`OpenaiToolDefinition` 结构体**定义于 `llm/types/tool.rs`,包含 4 个字段: - `name: String` - `description: Option` - `parameters: Value` - `strict: Option` 当前存在多处 `#[allow(deprecated)]` 抑制点,分布在 `llm/cycle.rs`、`tools/registry.rs`、`tools/mcp.rs`、`agent/agent.rs` 等文件中。 2. **`ToolDefinition` 类型别名**定义于 `llm/types/mod.rs:105`,标记为 `#[deprecated]`: ```rust #[deprecated(since = "0.1.0", note = "use OpenaiToolDefinition directly")] pub type ToolDefinition = OpenaiToolDefinition; ``` 3. **`MessageRequest.tools` 字段**类型为 `Vec`(直接引用原始类型,而非别名)。 4. **引用该类型的 4 个源文件**: - `llm/cycle.rs`:4 个方法参数使用 `Vec` - `tools/registry.rs`:`definitions() -> Vec` 返回类型 + struct literal 构造(含 `strict: None`) - `tools/mcp.rs`:`list_tools() -> Vec` 返回类型 + struct literal 构造(含 `strict: None`) - `agent/agent.rs`:`fn tool_definitions() -> Vec` trait 默认实现 5. **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` 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` 同文件,不建独立文件/模块)。 ```rust #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ToolDef { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(default)] pub parameters: Value, } ``` 实现双向 `From` 转换: ```rust impl From for OpenaiToolDefinition { fn from(t: ToolDef) -> Self { Self { name: t.name, description: t.description, parameters: t.parameters, strict: None, } } } impl From 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`;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(|t| OpenaiTool::Function { function: t })` → 改为 `.map(|t| OpenaiTool::Function { function: t.into() })`;`t` 类型从 `OpenaiToolDefinition` 变为 `ToolDef`,需 `Into` 转换 | | 验证标准 | `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` → 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 兼容。 ## 实施步骤 1. 新建分支 `phase-6-tooldef-ir` 2. 按单元 6.1 → 6.2 → 6.3 → 6.4 顺序执行,每步提交一个 commit 3. 每步执行对应的验证标准 4. 全量通过后创建 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+ 阶段处理 |