- 各写法使用的版本见 frontmatter,与对应专题笔记保持一致。所有 Python 代码都用 pyright、所有 TS 代码都用 tsc strict 模式对照这些版本做过类型检查。
- 由于没有各家的 API Key,以下代码都没有调用真实模型。不过,pi 和 DSH 的写法已经在 4.3 pi-agent-core 最小 Agent 运行时 和 5.3 DSH 核心机制与插件开发 中,用 faux 模型或真实的工具流水线实际跑通过。
- 资料截至 2026-10-09。
用同一个任务——“查询北京和上海的天气,并回答问题”,需要一个自定义工具 get_weather(city)——分别用六种方式实现,对比谁在写循环、工具怎么定义、状态放在哪里:
| # | 写法 | 所在层 | 语言 |
|---|---|---|---|
| ① | OpenAI Responses API + 手写循环 | L1/L2 | Python |
| ② | OpenAI Agents SDK | L3 | Python |
| ③ | Anthropic Tool Runner | L3 | Python |
| ④ | Claude Agent SDK | L4 | Python |
| ⑤ | pi-agent-core | L3 | TypeScript |
| ⑥ | DSH 工具插件 + SDK | L4 | TypeScript |
公共部分:工具的“业务逻辑”#
六种写法都使用同一份假数据:
WEATHER = {"北京": "晴,25°C", "上海": "小雨,22°C"}get_weather(city) -> WEATHER.get(city, "暂无数据")① OpenAI Responses API + 手写循环#
import jsonfrom typing import Any
from openai import OpenAIfrom openai.types.responses import FunctionToolParam
WEATHER = {"北京": "晴,25°C", "上海": "小雨,22°C"}client = OpenAI()TOOLS: list[FunctionToolParam] = [ { "type": "function", "name": "get_weather", "description": "查询某个城市今天的天气", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], "additionalProperties": False, }, "strict": True, }]
history: list[Any] = [{"role": "user", "content": "北京和上海今天天气怎么样?"}]while True: r = client.responses.create(model="gpt-6-astra", instructions="你是简洁的中文助手。", tools=TOOLS, input=history) history += r.output calls = [i for i in r.output if i.type == "function_call"] if not calls: print(r.output_text) break for c in calls: city = json.loads(c.arguments)["city"] history.append({"type": "function_call_output", "call_id": c.call_id, "output": WEATHER.get(city, "暂无数据")})特点:
- 什么都要自己做:手写 JSON Schema、循环、
call_id关联、历史管理; - 但也完全透明,详细讲解见 1.2 Tool Calling 与 Agent Loop 原理。
- 这个精简版省略了
max_turns安全阀,生产代码不要省。
② OpenAI Agents SDK#
from agents import Agent, Runnerfrom agents.decorators import tool
WEATHER = {"北京": "晴,25°C", "上海": "小雨,22°C"}
@tooldef get_weather(city: str) -> str: """查询某个城市今天的天气。
Args: city: 城市名,例如 北京。 """ return WEATHER.get(city, "暂无数据")
agent = Agent(name="天气助手", instructions="你是简洁的中文助手。", tools=[get_weather])result = Runner.run_sync(agent, "北京和上海今天天气怎么样?")print(result.final_output)特点:
- 函数签名和 docstring 自动生成工具 schema;
- 循环由
Runner负责; - 需要的时候,可以随时加上
output_type、guardrails、handoffs、session、tracing。
③ Anthropic Tool Runner(beta)#
from anthropic import Anthropic, beta_tool
WEATHER = {"北京": "晴,25°C", "上海": "小雨,22°C"}client = Anthropic()
@beta_tooldef get_weather(city: str) -> str: """查询某个城市今天的天气。
Args: city: 城市名,例如 北京。 """ return WEATHER.get(city, "暂无数据")
runner = client.beta.messages.tool_runner( model="claude-opus-5-5", max_tokens=16000, system="你是简洁的中文助手。", tools=[get_weather], messages=[{"role": "user", "content": "北京和上海今天天气怎么样?"}],)final = runner.until_done()print("".join(b.text for b in final.content if b.type == "text"))特点:
- 写法和 ② 很像,都是用装饰器定义工具、由 SDK 跑循环;
- 但它仍然是 Messages API 的一层薄封装,没有 agent、handoff、session 这些概念,也没有内置工具。
详见 3.2 Anthropic 客户端 SDK 与 Messages API。
④ Claude Agent SDK#
import asynciofrom typing import Annotated, Any
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, create_sdk_mcp_server, query, tool
WEATHER = {"北京": "晴,25°C", "上海": "小雨,22°C"}
@tool("get_weather", "查询某个城市今天的天气", {"city": Annotated[str, "城市名,例如 北京"]})async def get_weather(args: dict[str, Any]) -> dict[str, Any]: return {"content": [{"type": "text", "text": WEATHER.get(args["city"], "暂无数据")}]}
async def main() -> None: options = ClaudeAgentOptions( system_prompt="你是简洁的中文助手。", mcp_servers={"weather": create_sdk_mcp_server(name="weather", version="1.0.0", tools=[get_weather])}, allowed_tools=["mcp__weather__get_weather"], # 预先批准,不弹审批 tools=[], # 为了公平对比,移除所有内置工具(Read、Bash 等) ) async for message in query(prompt="北京和上海今天天气怎么样?", options=options): if isinstance(message, ResultMessage) and message.subtype == "success": print(message.result)
asyncio.run(main())特点:
- 自定义工具通过进程内的 MCP server 提供,工具名格式是
mcp__{server}__{tool}; - 内部会启动一个
claudeCLI 子进程来执行循环; - 如果不传
tools=[],agent 默认就拥有读写文件、执行命令等一整套内置工具,这才是它真正的价值所在。
⑤ pi-agent-core(TypeScript)#
import { Agent, type AgentTool } from "@earendil-works/pi-agent-core";import { createModels, Type } from "@earendil-works/pi-ai";import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
const WEATHER: Record<string, string> = { 北京: "晴,25°C", 上海: "小雨,22°C" };
const models = createModels();models.setProvider(anthropicProvider()); // 换成 openaiProvider() 或 deepseekProvider(),其余代码都不用改const model = models.getModel("anthropic", "claude-opus-5-5")!;
const params = Type.Object({ city: Type.String({ description: "城市名,例如 北京" }) });const getWeather: AgentTool<typeof params> = { name: "get_weather", label: "查询天气", description: "查询某个城市今天的天气", parameters: params, execute: async (_id, { city }) => ({ content: [{ type: "text", text: WEATHER[city] ?? "暂无数据" }], details: {} }),};
const agent = new Agent({ initialState: { systemPrompt: "你是简洁的中文助手。", model, tools: [getWeather] }, streamFn: models.streamSimple.bind(models),});agent.subscribe((e) => { if (e.type === "message_update" && e.assistantMessageEvent.type === "text_delta") { process.stdout.write(e.assistantMessageEvent.delta); // 流式输出 }});await agent.prompt("北京和上海今天天气怎么样?");特点:
- 工具用 TypeBox 定义,同时得到类型推导和运行时校验;
- 模型提供方可以随意替换;
- 通过细粒度的事件流实现流式输出,还可以加 steering、follow-up、hooks。
把这段代码里的提供方换成 faux 模型后,笔者已在本地实际运行过,见 4.3 pi-agent-core 最小 Agent 运行时 › 1. 最小示例(✅ 已实际运行)。
⑥ DSH:工具插件 + SDK(TypeScript)#
DSH 的思路与前五种完全不同:工具是一个插件,通过配置组合进 agent,而不是作为参数传给 agent。
第 1 步:写一个工具插件。 这里沿用了 5.3 DSH 核心机制与插件开发 中实际运行过的写法:
import type { Context } from '@deepseek-ai/cordis'import { defineTool } from '@deepseek-ai/dsh-tools'
const WEATHER: Record<string, string> = { 北京: '晴,25°C', 上海: '小雨,22°C' }
export const name = 'weather-tool'export const inject = ['tools']
export function apply(ctx: Context) { ctx.tools.register(defineTool({ name: 'get_weather', description: '查询某个城市今天的天气', parameters: { city: { type: 'string', required: true, description: '城市名,例如 北京' } }, output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] }, async execute(args) { return WEATHER[args.city] ?? '暂无数据' }, }))}笔者另写了一个 driver 插件,通过 ctx.tools.execute() 调用这个工具,让它走一遍真实的 dsh-tools 执行流水线(不调用模型)。输出如下:
北京 -> [{"type":"text","text":"晴,25°C"}]上海 -> [{"type":"text","text":"小雨,22°C"}]广州 -> [{"type":"text","text":"暂无数据"}]第 2 步:把插件组合进 profile。 写一个 patch overlay,插入一个新的插件条目:
# weather.cordis.yml(示意:格式参照仓库中 presets/*.patch.yml 的 insert 写法,笔者没有实际运行过)- insert: - id: weather-tool name: './weather-tool.ts'第 3 步:用 SDK 驱动。 下面是仓库源码的 API;npm 上 0.0.1-rc.1 的写法见 5.1 DeepSeek Harness 全景 › 6. 两个 SDK:用代码驱动 DSH:
// 片段(no-check):仓库源码 API,npm 0.0.1-rc.1 尚不支持 profile 和 patches 选项import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
await using harness = new DeepSeekHarness({ profile: 'sdk', patches: ['./weather.cordis.yml'], // 叠加我们的插件 provider: 'deepseek-official', model: 'deepseek-v4-flash',})const result = await harness.run('北京和上海今天天气怎么样?')console.log(result.finalResponse)特点:
- 工具、模型适配器、循环、沙箱全都是插件。改变 agent 的能力,靠的是改配置而不是改代码。
- 这个工具会自动获得审批、沙箱、超时、审计、PTC 调用等能力,因为这些都由流水线上的其他插件提供。
- 代价是要学习的概念最多:profile、bundle、patch、inject、waterfall 等。
详见 5.1 DeepSeek Harness 全景、5.2 Cordis 与一切皆插件、5.3 DSH 核心机制与插件开发。
对比#
| 维度 | ① Responses 手写 | ② Agents SDK | ③ Tool Runner | ④ Claude Agent SDK | ⑤ pi-agent-core | ⑥ DSH |
|---|---|---|---|---|---|---|
| 谁来写循环 | 你 | Runner | tool_runner | Claude Code CLI 子进程 | Agent | agent-loop 插件 |
| 工具怎么定义 | 手写 JSON Schema | 装饰器,基于签名和 docstring | 装饰器,基于签名和 docstring | @tool 加进程内 MCP | TypeBox 对象 | defineTool 加插件 |
| 工具 schema 怎么生成 | 手写 | 自动 | 自动 | dict 自动转换,或者手写 JSON Schema | TypeBox | 参数规格自动转换 |
| 状态放在哪 | 你手里的 list | Session 或服务端状态 | runner 内部 | 本地 JSONL 会话 | AgentState | 事件溯源日志 |
| 内置工具 | 托管工具,可选 | 托管工具和 Sandbox,可选 | 无 | 一整套编码工具 | 无(在 L4 的 pi-coding-agent 里才有) | 由插件提供,有 preset |
| 换模型厂商 | 不行 | 可以,主要是兼容 OpenAI 的厂商 | 不行 | 不行 | 可以,数十家 | 可以,通过适配器插件 |
| 学习成本 | 低,但写的代码多 | 低 | 低 | 中 | 中 | 高 |
| 适合什么场景 | 学习、极致掌控 | 业务流程编排 | Claude 加自定义工具 | 干活型 agent | 自研框架、多模型 | 可深度组装的平台 |
这张表是笔者的归纳。
选型的本质是:你愿意把哪几层交给别人? 交出去的越多,写的代码越少,但你能掌控的也越少。完整的选型建议见 6.2 横向对比与选型建议。
小结#
- 六种写法的差别集中在三个问题上:谁来写循环、工具怎么定义、状态放在哪里。
- 从 ① 往后,每一步都多交出去一部分:先交出循环(② ③ ⑤),再交出工具、会话和权限(④ ⑥)。
- 工具的业务逻辑在六种写法里完全相同,变的只是外面那层“胶水”。所以先把工具写成与 SDK 无关的普通函数,以后换 SDK 的成本最低(笔者观点)。
相关笔记#
- 1.2 Tool Calling 与 Agent Loop 原理 · 6.2 横向对比与选型建议
- 各写法对应的笔记:2.3 OpenAI Agents SDK · 3.2 Anthropic 客户端 SDK 与 Messages API · 3.3 Claude Agent SDK · 4.3 pi-agent-core 最小 Agent 运行时 · 5.3 DSH 核心机制与插件开发
参考资料#
各写法的 API 出处见对应专题笔记的参考资料,这里只列出主要入口:
- OpenAI Function calling:https://developers.openai.com/api/docs/guides/function-calling
- OpenAI Agents SDK:https://github.com/openai/openai-agents-python/blob/26345c1e45ebede8e2fc9b0bc7341dedab5e01fc/docs/tools.md
- Anthropic Tool Runner:https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner
- Claude Agent SDK custom tools:https://code.claude.com/docs/en/agent-sdk/custom-tools
- pi-agent-core:https://github.com/earendil-works/pi/blob/6fb2e7815167e6b19006fc526d1a5d0f5f998787/packages/agent/README.md
- DSH 工具编写参考:https://github.com/deepseek-ai/deepseek-harness/blob/5badb15009ae1756c3afe0ae0cef1faafc290ccc/docs/cookbook/adding-a-tool.zh.md