返回专栏
Agent SDK/06 · 对比/6.1

同一个任务的六种写法

用同一个任务——“查询北京和上海的天气,并回答问题”,需要一个自定义工具 get_weather(city)——分别用六种方式实现,对比谁在写循环、工具怎么定义、状态放在哪里: | # | 写法 | 所在层 | 语言 | |---|---|…

预计阅读
16分钟
全文字数
1,632字
资料截至
2026-10-09
Agent SDK · 对比6.1
版本与时效声明
  • 各写法使用的版本见 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/L2Python
②OpenAI Agents SDKL3Python
③Anthropic Tool RunnerL3Python
④Claude Agent SDKL4Python
⑤pi-agent-coreL3TypeScript
⑥DSH 工具插件 + SDKL4TypeScript

公共部分:工具的“业务逻辑”#

六种写法都使用同一份假数据:

WEATHER = {"北京": "晴,25°C", "上海": "小雨,22°C"}
get_weather(city) -> WEATHER.get(city, "暂无数据")

① OpenAI Responses API + 手写循环#

import json
from typing import Any
from openai import OpenAI
from 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, Runner
from agents.decorators import tool
WEATHER = {"北京": "晴,25°C", "上海": "小雨,22°C"}
@tool
def 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。

详见 2.3 OpenAI Agents SDK。


③ Anthropic Tool Runner(beta)#

from anthropic import Anthropic, beta_tool
WEATHER = {"北京": "晴,25°C", "上海": "小雨,22°C"}
client = Anthropic()
@beta_tool
def 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 asyncio
from 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};
  • 内部会启动一个 claude CLI 子进程来执行循环;
  • 如果不传 tools=[],agent 默认就拥有读写文件、执行命令等一整套内置工具,这才是它真正的价值所在。

详见 3.3 Claude Agent SDK。


⑤ 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 核心机制与插件开发 中实际运行过的写法:

weather-tool.ts
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] ?? '暂无数据'
},
}))
}
第 1 步已实际运行

笔者另写了一个 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 核心机制与插件开发。


对比#

交出循环

再交出工具、会话、权限

完整 harness(L4)

④ Claude Agent SDK

⑥ DSH

SDK 负责循环(L3)

② Agents SDK

③ Tool Runner

⑤ pi-agent-core

你自己写循环

① Responses + 手写

交出循环

再交出工具、会话、权限

完整 harness(L4)

④ Claude Agent SDK

⑥ DSH

SDK 负责循环(L3)

② Agents SDK

③ Tool Runner

⑤ pi-agent-core

你自己写循环

① Responses + 手写

维度① Responses 手写② Agents SDK③ Tool Runner④ Claude Agent SDK⑤ pi-agent-core⑥ DSH
谁来写循环你Runnertool_runnerClaude Code CLI 子进程Agentagent-loop 插件
工具怎么定义手写 JSON Schema装饰器,基于签名和 docstring装饰器,基于签名和 docstring@tool 加进程内 MCPTypeBox 对象defineTool 加插件
工具 schema 怎么生成手写自动自动dict 自动转换,或者手写 JSON SchemaTypeBox参数规格自动转换
状态放在哪你手里的 listSession 或服务端状态runner 内部本地 JSONL 会话AgentState事件溯源日志
内置工具托管工具,可选托管工具和 Sandbox,可选无一整套编码工具无(在 L4 的 pi-coding-agent 里才有)由插件提供,有 preset
换模型厂商不行可以,主要是兼容 OpenAI 的厂商不行不行可以,数十家可以,通过适配器插件
学习成本低,但写的代码多低低中中高
适合什么场景学习、极致掌控业务流程编排Claude 加自定义工具干活型 agent自研框架、多模型可深度组装的平台

这张表是笔者的归纳。

怎么选

选型的本质是:你愿意把哪几层交给别人? 交出去的越多,写的代码越少,但你能掌控的也越少。完整的选型建议见 6.2 横向对比与选型建议。

小结#

  • 六种写法的差别集中在三个问题上:谁来写循环、工具怎么定义、状态放在哪里。
  • 从 ① 往后,每一步都多交出去一部分:先交出循环(② ③ ⑤),再交出工具、会话和权限(④ ⑥)。
  • 工具的业务逻辑在六种写法里完全相同,变的只是外面那层“胶水”。所以先把工具写成与 SDK 无关的普通函数,以后换 SDK 的成本最低(笔者观点)。

相关笔记#

参考资料#

各写法的 API 出处见对应专题笔记的参考资料,这里只列出主要入口:

输入关键词开始搜索。多个关键词用空格分隔。