Files
agcore/design/pdd/13-phase6-tooldef-ir.md
徐涛 28ca43ccb2 chore(docs): 将设计文档从 docs 移至 design 目录
将 note、pdd、prd、roadmap 四类文档分别归入 `design/` 下对应子目录中,并新增 `.gitkeep` 占位文件
2026-07-23 05:45:53 +08:00

11 KiB
Raw Permalink Blame History

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.rstools/registry.rstools/mcp.rsagent/agent.rs 等文件中。

  2. ToolDefinition 类型别名定义于 llm/types/mod.rs:105,标记为 #[deprecated]

    #[deprecated(since = "0.1.0", note = "use OpenaiToolDefinition directly")]
    pub type ToolDefinition = OpenaiToolDefinition;
    
  3. MessageRequest.tools 字段类型为 Vec<OpenaiToolDefinition>(直接引用原始类型,而非别名)。

  4. 引用该类型的 4 个源文件

    • llm/cycle.rs4 个方法参数使用 Vec<ToolDefinition>
    • tools/registry.rsdefinitions() -> Vec<ToolDefinition> 返回类型 + struct literal 构造(含 strict: None
    • tools/mcp.rslist_tools() -> Vec<ToolDefinition> 返回类型 + struct literal 构造(含 strict: None
    • agent/agent.rsfn tool_definitions() -> Vec<ToolDefinition> trait 默认实现
  5. Provider 适配层openai.rsanthropic.rsMessageRequest.tools 读取数据并转换为各自的 wire format。openai_compat.rsollama.rs 委托给 GenericOpenaiProvider,无需直接改动。

需求推演

决策 1strict 字段的处理

当前所有构造路径均硬编码 strict: Noneregistry.rsmcp.rs),BaseTool trait 无 strict 方法,用户 API 无法设置该值。

结论:移除。 strict 是 OpenAI 的 Structured Outputs 专属字段,不属于 Provider 无关的 IR。未来如需支持,走 MessageRequest.extra 逃生舱,在各 Provider 适配层自行消费。

决策 2description 保持 Option<String>

Anthropic 要求 description 为必填(String),但 MCP 等来源可能缺失该字段。保持 Option,由 Anthropic 适配层以 unwrap_or_default() 兜底。

决策 3parameters 保持 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.rsllm/types/request_v2.rsllm/types/request.rstools/registry.rstools/mcp.rs
变更内容 切换别名 pub type ToolDefinition = ToolDefMessageRequest.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.rstools/registry.rstools/mcp.rsagent/agent.rsllm/types/mod.rsllm/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 适配方式 改动
OpenAIGenericOpenaiProvider 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 CompatDeepSeek、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::ToolDefOpenaiToolDefinition 已降级为 #[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/ 模块 会创造 llmtools 的逆向依赖,破坏模块分层
添加 builder 模式 Rust struct literal + ..Default::default() 已足够覆盖使用场景
添加 #[non_exhaustive] IR 类型自有完整控制权,不需要对外隐藏字段
同 Phase 净化 parameters 类型化 属于独立工作,留给 v0.3+ 阶段处理