为 Agent 添加类型化工具
通过 JSON Schema 暴露异步 Rust 函数,同时避免授予环境中的隐式权限。
定义工具
Runifold Tool 具有稳定描述、JSON 输入 Schema、副作用分类与异步执行边界。普通的
类型化函数优先使用 #[runifold::tool] 宏。
先加入派生宏依赖:
cargo add serde --features derive
cargo add schemarsuse runifold::{JsonSchema, ToolContext, ToolError, tool};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, JsonSchema)]
struct WeatherInput {
city: String,
}
#[derive(Debug, Serialize, JsonSchema)]
struct Weather {
city: String,
celsius: i32,
}
#[tool(description = "Get the current temperature for a city")]
async fn current_weather(
input: WeatherInput,
_context: ToolContext,
) -> Result<Weather, ToolError> {
weather_service::lookup(&input.city).await
}输入应该使用领域类型,而不是到处传递字符串集合。校验应发生在边界上,并且早于 任何外部工作。
注册工具
只在真正需要它的 Agent 上注册工具:
let agent = runtime
.agent("travel-planner")
.tool(current_weather_tool())
.max_turns(6)
.build()?;重复名称,以及与子 Agent 路由同名,都会产生构建错误。工具被注册后也不会自动对 所有子 Run 可用;执行时仍然需要对应的 Capability 授权。
返回富结果
从 0.9.0 开始,手写 Tool 返回 ToolOutput,不再把所有结果压成一个标量。应根据
真实边界选择构造器:
| 需求 | 构造器 | 语义 |
|---|---|---|
| 普通 JSON 或文本 | ToolOutput::model_visible(value) | 同时保留结构化 JSON 与文本回退 |
| 有序文本、图片、音频、文档或资源 | ToolOutput::rich(parts) | 保留与 Provider 无关的内容顺序 |
| 可恢复的领域失败 | ToolOutput::model_error(parts) | Tool 已执行,但应用拒绝请求;模型可以处理 |
| 仅宿主可见的数据 | ToolOutput::host_only(parts) | 如果代码试图暴露给模型,会失败关闭 |
用 with_structured_content 附加独立验证的输出,用带命名空间的 with_metadata 保存
应用侧注释。ToolError 仍表示 Tool Runtime 执行失败;model_error 则是已经完成、允许
模型看到的应用结果。
从 0.3.x 升级时,手写的 ToolOutput { value, ... } 必须迁移到上述构造器之一。
让二进制 Artifact 远离 Transcript
大型 Tool 输出在 Conversation 与 Checkpoint 中应该保存引用,而不是 Base64。先在
Agent 上配置一个 ArtifactStore 与经过校验的 Scope:
use std::sync::Arc;
use runifold::{ArtifactScope, InMemoryArtifactStore};
let artifact_store = Arc::new(InMemoryArtifactStore::new());
let agent = runtime
.agent("report-analyst")
.artifacts(ArtifactScope::parse("tenant.acme")?, artifact_store)
.tool(render_chart_tool())
.build()?;Tool 内部从 ToolContext 取得 Store 与 Scope,使用稳定的幂等键写入,再把生成的
ArtifactRef 作为 MediaSource::Artifact 返回。Runifold 只会在最终 Provider 传输边界
加载并校验字节。
测试使用 InMemoryArtifactStore,单个持久进程使用 SqliteStore,共享生产 Worker 使用
PostgresStore。每个引用都会绑定 Scope、MIME、字节长度、SHA-256、可选名称与过期时间;
分页、删除和过期清理也不会越过 Scope。
Provider 支持是显式的:OpenAI Responses 与 Gemini 保留原生多模态 Tool Result;Anthropic 支持文本、图片和资源;Bedrock 支持 JSON、图片和文档。纯文本 Chat Completions 与 Ollama 遇到不支持的媒体会明确报错,不会悄悄转成字符串。
权限与副作用
工具注册回答“这个 Agent 可能调用什么”,RunContext Capability 回答“这次执行
允许调用什么”,两者必须同时满足。
外部动作应该被准确分类:
- 纯读取通常可以安全重试;
- 幂等写入需要稳定的幂等键;
- 非幂等写入在传输结果不确定时必须失败关闭;
- 删除或高风险操作应该通过应用策略审核。
需要恢复的写操作应使用预写 Effect 边界:先记录执行意图,再进行外部调用,并在 结果已经完成时直接重放结果,而不是重复执行动作。
处理工具错误
ToolErrorPolicy 决定工具失败是作为模型可见输入,还是直接终止 Agent。模型可见
错误不能包含 Secret、原始凭证、数据库 URL 或内部堆栈。
运维错误应该保留足够结构,让应用能够决定重试、补偿、请求人工介入或终止 Run。