28ca43ccb2
将 note、pdd、prd、roadmap 四类文档分别归入 `design/` 下对应子目录中,并新增 `.gitkeep` 占位文件
237 lines
11 KiB
Markdown
237 lines
11 KiB
Markdown
# 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<String>`
|
||
- `parameters: Value`
|
||
- `strict: Option<bool>`
|
||
|
||
当前存在多处 `#[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<OpenaiToolDefinition>`(直接引用原始类型,而非别名)。
|
||
|
||
4. **引用该类型的 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 默认实现
|
||
|
||
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<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` 同文件,不建独立文件/模块)。
|
||
|
||
```rust
|
||
#[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` 转换:
|
||
|
||
```rust
|
||
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(|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<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 兼容。
|
||
|
||
## 实施步骤
|
||
|
||
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+ 阶段处理 |
|