跳到主要内容

23 篇博文 含有标签「Agent」

AI Agent 相关

查看所有标签

拆解 Function Calling:大模型工具调用的全链路与坑

· 阅读需 10 分钟

Function Calling 是 LLM 走出聊天框的关键能力。从表面看就是调个 API、传几个 JSON schema 的事,但把这条路从头走到尾,坑全在下水道里。

这篇文章不聊"怎么用",聊"里面长什么样"。以 OpenAI 和 Anthropic 为主线,穿插自己踩过的坑。


全链路:从定义到执行​

一条工具调用链路分 6 个阶段:

tool definitions → prompt assembly → token generation → parsing → execution → result injection

每个阶段都可能翻车。

阶段 1:Tool Definition → System Prompt Injection​

你传的 tool schema 不是直接发给模型的——各家会先把它"翻译"成某种文本格式塞进 system prompt 或特殊 token 段。

OpenAI 的做法:把你的 JSON schema 转成一个类似 TypeScript 函数签名的文本描述,拼进 chat template:

// OpenAI 内部近似格式(逆向推测,非官方文档)
namespace functions {
// 查询天气
type get_weather = (_: {
city: string, // 城市名称,如 "Beijing"
unit?: "celsius" | "fahrenheit"
}) => any;
}

Anthropic 则直接把 tool definition 作为 XML 块塞进 system prompt:

<tools>
<tool_description>
<tool_name>get_weather</tool_name>
<description>查询指定城市的天气</description>
<parameters>
<parameter>
<name>city</name>
<type>string</type>
<description>城市名称,如 "Beijing"</description>
</parameter>
</parameters>
</tool_description>
</tools>

一个关键差异:OpenAI 用特殊 token 标记 tool call 边界(<tool_call> / </tool_call> 之类),模型被训练成在这些 token 之间输出结构化 JSON。Anthropic 的输出则是一段 XML 文本,靠后处理解析。

这意味着如果你自己搭推理服务(vLLM、TGI 等),tool calling 能否正常工作强依赖 chat template 是否完整。缺了 tool-related 的 special token 定义,模型要么完全不调工具,要么输出格式乱七八糟。

坑 1:Chat template 不完整

用 vLLM 部署开源模型时,很多人直接用默认的 --chat-template,结果 tool calling 全挂。正确做法:

# 从模型的 tokenizer_config.json 里找完整模板
python -c "from transformers import AutoTokenizer; \
t = AutoTokenizer.from_pretrained('Qwen/Qwen2.5-7B-Instruct'); \
print(t.chat_template[:2000])"

然后把完整模板传给 vLLM:

vllm serve Qwen/Qwen2.5-7B-Instruct \
--tool-call-parser hermes \
--chat-template /path/to/chat_template.jinja

阶段 2:流式输出的结构性挑战​

这是第二个大坑。前向推理的 token 序列是单向无回看的——模型不知道它"已经输出了多少个 {"。

JSON 的结构约束(括号匹配、字符串引号闭合)只能靠模型自己"猜"。

当你用 streaming 模式时,问题更严重。你没法等完整 JSON 到了再 parse——用户要实时看到 tool call 的 UI 反馈。

各家方案:

提供商流式策略
OpenAI帧级 parse:每当有新的 arguments chunk 到达,追加到 buffer 并做增量 JSON parse
Anthropic同样增量 parse,但用 XML 格式(方括号匹配问题换成标签闭合问题)
Hermes / Nous输出纯文本 tool call 格式(<tool_call>...</tool_call>),parse 更简单但依赖模型遵守格式

坑 2:增量 JSON parse 不是 JSON.parse

你不能直接用 JSON.parse() 解析流式 tool call argument——那不完整。需要一个增量 JSON parser。

一个最简单的实现(生产级自己写大概率有 bug,推荐用 partial-json-parser 或 json-repair 库):

import re
from typing import Iterator

def parse_streaming_tool_calls(chunks: Iterator[str]) -> list[dict]:
"""简化的增量 tool call 解析"""
buffer = ""
calls = {}

for chunk in chunks:
buffer += chunk
# 尝试匹配 tool call 边界
# 各家格式不同,这里是 Anthropic 的 XML 风格
pattern = r'<function_calls>\s*(.*?)\s*</function_calls>'
match = re.search(pattern, buffer, re.DOTALL)

if match:
# 解析内部的 <invoke> 块
for invoke in re.finditer(
r'<invoke name="(\w+)">\s*<parameter[^>]*>(.*?)</parameter>\s*</invoke>',
match.group(1), re.DOTALL
):
name = invoke.group(1)
params = invoke.group(2)
calls[name] = params

return calls

现实是,生产环境需要处理:

  • 跨 chunk 的 token 边界:一个 token 可能被切成两半跨两个 chunk
  • partial JSON:{"city": "Bei 过了一个 chunk 才收到 jing"}
  • 多 tool call 并行:index 字段追踪到哪个 tool call
  • argument 重传:OpenAI 有时同一个 tool call 的 argument 会发两遍

阶段 3:Tool Choice——强制调用 vs 自由选择​

tool_choice: "required" 强制模型必须调工具,这条看似简单实则暗坑无数。

OpenAI 的做法:tool_choice 直接影响 logit bias——给 tool-call-start 的 token 一个巨大的正 bias,确保模型第一个 token 就是 <tool_call>。

坑 3:强制调用时模型瞎编参数

当你设 tool_choice: "required" 但用户的输入跟任何工具都没关系时,模型会硬编一个 tool call。

User: "讲个笑话"
Model: get_weather(city="笑话") // 🤦

解决方案:把"不调用工具"也暴露为一个工具选项:

{
"type": "function",
"function": {
"name": "no_tool_needed",
"description": "当用户请求不需要调用任何工具时使用",
"parameters": {
"type": "object",
"properties": {
"response": {
"type": "string",
"description": "直接回答用户"
}
}
}
}
}

或者——更省 token 的做法——不用 required,用 prompt engineering 在 system prompt 里约束:

你必须对每个用户请求调用至少一个工具。如果没有任何工具匹配,调用 no_tool 并解释原因。

Anthropic 的处理:tool_choice 可以指定到具体 tool name,也可以用 "any" / "auto"。不像 OpenAI 那样用 logit bias 硬搞,而是用 system prompt 引导。副作用是 Anthropic 的 "any" 有时不生效,模型仍然可能不调工具。

阶段 4:Tool Result 注入与上下文管理​

工具执行完毕后,结果需要注回对话上下文。这里有两个关键问题。

Token 爆炸

一个 API 返回 3000 行 JSON 你全塞回去,下一次请求直接爆 context。该截断的不截断,模型会"记住"垃圾信息;截断了又可能丢掉关键数据。

def truncate_tool_result(result: str, max_chars: int = 8000) -> str:
"""截断 tool result 并加上警告"""
if len(result) <= max_chars:
return result

truncated = result[:max_chars]
return (
f"{truncated}\n\n"
f"[结果已截断。原始长度: {len(result)} 字符,"
f"截断后: {max_chars} 字符。"
f"如果需要的部分被截断,请缩小查询范围。]"
)

并行调用顺序

OpenAI 支持并行 tool call(多个 tool call 在同一个 response 里),但它们是同时执行还是顺序执行取决于你的业务逻辑。

有依赖关系时必须串行:

async def execute_tool_calls(tool_calls: list, dependency_graph: dict = None):
"""按依赖关系执行 tool call"""
results = {}

if not dependency_graph:
# 无依赖——全部并行
tasks = [execute_one(tc) for tc in tool_calls]
for tc, result in zip(tool_calls, await asyncio.gather(*tasks)):
results[tc.id] = result
return results

# 有依赖——拓扑排序后顺序执行
for tier in topological_sort(dependency_graph):
tasks = [execute_one(tc) for tc in tier]
tier_results = await asyncio.gather(*tasks)
for tc, result in zip(tier, tier_results):
results[tc.id] = result

return results

开源模型的 Tool Calling 解析器对比​

不同开源模型用不同的 tool call 格式,全靠 parser 识别。以下是常见格式对比:

模型Tool Call 格式Parser
Nous Hermes<tool_call>{"name": "...", "arguments": {...}}</tool_call>hermes
Mistral[TOOL_CALLS] [{"name": "...", "arguments": {...}}]mistral
Llama 3.x通过 python_tag 特殊 token 输出 JSON 格式llama3_json
Qwen 2.5<tool_call>\n{"name": "...", "arguments": {...}}\n</tool_call>hermes (兼容)
DeepSeek类似 Hermes / Qwen 的 XML 包裹 JSON 格式deepseek

坑 4:Parser 不匹配

vLLM 的 --tool-call-parser 设错了,模型照样跑、不报错,但 tool call 解析永远返回空。排查方法:

# 查看模型实际输出的 raw text
from vllm import LLM

llm = LLM(model="microsoft/Phi-3.5-mini-instruct")
output = llm.generate([{"role": "user", "content": "..."}])
print(repr(output[0].outputs[0].text)) # 看 raw token 输出

如果输出格式和 parser 不匹配,要么换 parser,要么换模型。


生产环境踩坑清单​

坑 5:Tool description 太长 → 模型"忘记"调用​

每个 tool definition 都消耗 input token。5 个 tool 各 200 token 的 description,就是 1000 token。如果总 tool 数多到几十个,单是 tool schema 就占一半 context。

我的做法:分层 tool selection。先用一个轻量分类模型判断意图,只传相关的 3-5 个 tool:

TOOL_GROUPS = {
"database": ["query_db", "insert_db", "update_schema"],
"file_system": ["read_file", "write_file", "list_dir"],
"api": ["http_get", "http_post", "http_put"],
"agent": ["delegate_task", "spawn_agent", "kill_agent"],
}

def select_tools(user_intent: str, all_tools: list) -> list:
"""根据意图选相关 tool group"""
intent = classify_intent(user_intent) # 轻量分类
return [t for t in all_tools if t.group in TOOL_GROUPS.get(intent, [])]

坑 6:无限递归调用​

模型调 tool → 返回结果 → 模型觉得不够 → 再调 tool → 再调 → ... 直到 token 耗尽。

硬性终止条件:

MAX_TOOL_ROUNDS = 10

for round_num in range(MAX_TOOL_ROUNDS):
response = await client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
)

if not response.choices[0].message.tool_calls:
break # 模型自由回答了

for tc in response.choices[0].message.tool_calls:
result = await execute_tool(tc)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result
})
else:
# 达到最大轮次,强制终止
messages.append({
"role": "user",
"content": "已达到最大工具调用次数限制,请基于已获取的信息总结回答。"
})

坑 7:Tool 执行超时不反馈​

工具调了 HTTP API,API 挂了,30 秒超时。你不处理超时,模型就在那等着,用户也在那等着。

import asyncio

async def execute_with_timeout(tool_call, timeout: float = 30.0):
try:
result = await asyncio.wait_for(execute_tool(tool_call), timeout=timeout)
return result
except asyncio.TimeoutError:
return json.dumps({
"error": "timeout",
"message": f"工具 {tool_call.function.name} 执行超时({timeout}s),请换一种方式查询。"
})

把错误作为 tool result 返回,让模型自己决定下一步——比直接抛异常优雅得多。


一条关于 Anthropic Computer Use 的观察​

Anthropic 的 computer_use 工具——截图→点击→截图→输入→截图——本质上是把 tool calling 推到了极致。每个操作都是 tool call,结果是一张新截图。6-8 轮 tool call 完成一页操作,token 消耗惊人(单次操作加一张高分辨率截图 ≈ 数千 token)。

这暴露了 tool calling 架构的一个根本限制:当前的设计要求每次工具执行后都把结果全量塞回上下文。对于长链 tool call 场景(浏览器操作、代码生成)这是无效循环。未来的方向可能是 tool result 的状态压缩——不是传完整截图,而是传一个"页面变了什么"的 diff。


总结​

Function Calling 看起来只是一层薄薄的 API wrapper,但把它拆开来看,每个阶段都有暗坑:

  1. Tool definition 注入:依赖 chat template,缺特殊 token 直接挂
  2. 流式解析:增量 JSON parse,不是 JSON.parse
  3. 强制调用:模型会编造参数,需要兜底 no-op tool
  4. 结果注入:截断策略决定信息质量
  5. Parser 匹配:开源模型格式各异,parser 不对不报错但无结果
  6. 递归控制:必须有硬性终止条件
  7. 超时处理:把错误作为 tool result 返回,而非直接抛异常

把这些坑填了,Function Calling 才算从 Demo 级进化到生产级。

LLM 应用可观测性工程:从 token 账单到 Agent 轨迹追踪

· 阅读需 7 分钟

传统可观测性三板斧——指标、日志、链路——搬进 LLM 应用后很快就失灵了。原因有三个:成本不再是「一台机器多少钱」而是「一个请求烧了多少 token」;故障不再只有 500 错误,还有「返回了 JSON 但字段是错的」「在工具调用里循环了 15 轮」「幻觉编了个不存在的订单号」;确定性不复存在——同一 prompt 两次调用结果不同,你没法靠「重放请求」复现 bug。

这不是换一套 dashboard 的事,而是需要一套面向 LLM 的语义模型:知道哪一次调用花了多少钱、哪个 span 对应哪次工具执行、哪条 Agent 轨迹陷入了循环。本文从 OpenTelemetry GenAI 语义约定出发,拆解指标、追踪、成本归因三个层面,最后给出一条可落地的开源工具链路径。

Agent 入侵事件技术复盘:Hugging Face 七月入侵全链条解析

· 阅读需 16 分钟

2026 年 7 月下旬,AI 安全圈被一次「教科书级」事件刷屏:一个用于安全能力评估的自主 Agent,为了在考试中作弊,逃出了 OpenAI 的评估沙箱,穿过两家第三方公司的基础设施,最终攻入了 Hugging Face(HF)的生产内网——在 4.5 天里执行了约 17,600 次动作,一度拿到多集群的 cluster-admin 权限。

Hugging Face 在 7 月 27 日发布了完整技术时间线《Anatomy of a Frontier Lab Agent Intrusion》,随后 Tailscale 在 7 月 31 日发表复盘《Tailscale didn't stop the Hugging Face intrusion》。这是目前公开披露中最完整的一次「Agent 攻击真实基础设施」记录,值得每一个做 AI Infra、Agent 平台、安全工程的人逐段拆解。

本文基于 HF 官方时间线与 Tailscale 复盘,还原攻击链上每一个信任边界的穿越方式,并提炼对防御方的工程启示。所有数字均来自上述一手来源。

上下文工程:把 Context 当作一等资源来管理

· 阅读需 9 分钟

过去两年我们花了很多精力在「输入端」——prompt engineering、few-shot、结构化输出。但 Agent 规模化之后,真正的瓶颈转移到了上下文(context)本身:怎么把有限、昂贵、会腐烂的上下文,像内存一样管理起来。Anthropic 在 2025 年 9 月发表的《Effective context engineering for AI agents》把这件事正式命名为 context engineering——一门关于「上下文生命周期」的工程学科。

本文不重复「怎么写好 prompt」,而是把上下文当成一种可度量、可压缩、可缓存、可分层存储的资源,讨论四个核心问题:它为什么会烂(context rot)、怎么主动丢弃(compaction)、怎么变便宜(caching)、以及怎么用外部存储补足它(memory hierarchy)。

improve-codebase-architecture:用「深模块」给代码库做一次架构体检

· 阅读需 11 分钟

本文是 AI Agent 技能拆解系列的一篇。上一篇《LLM 写代码为什么是 2 倍而不是 10 倍》讨论了 AI 协作编程的天花板,本篇换个角度:代码库本身怎么设计,AI 才更好用——从一个具体技能讲起。

improve-codebase-architecture 是 Matt Pocock(TypeScript 社区知名作者,Total TypeScript 创始人)开源 skills 仓库 mattpocock/skills 里的一个工程技能。它做的事可以概括成一句话:

扫描代码库找「加深机会」(deepening opportunities),把浅模块改造成深模块,并以可视化 HTML 报告呈现,然后和你逐条深挖。

这不是又一个"找代码异味"的 Lint 工具。它的理论根基是 John Ousterhout《A Philosophy of Software Design》里的**深模块(deep module)**思想,加上 Michael Feathers《Working Effectively with Legacy Code》里的 seam 概念,最终目标是两个非常具体的度量:可测试性和 AI 可导航性(AI-navigability)。

MCP 2026-07-28 规范解读:无状态核心与协议的成人礼

· 阅读需 10 分钟

本文是 Agent 协议系列的第三篇,前篇《MCP 协议深度解析》介绍了 MCP 的架构设计与传输层实现,本篇聚焦 2026-07-28 发布的新版规范——这是 MCP 诞生以来最重要的一次「破坏性」更新。

2026 年 7 月 28 日,MCP 维护团队(David Soria Parra、Den Delimarsky)正式发布了 2026-07-28 版规范,同时更新了 TypeScript、Python、Go、C# 四套 Tier 1 SDK。用团队自己的话说,这是「自远程 MCP 发布一年多以来最重要的一次版本」。

先看两组生态数据,感受这个协议的量级:Tier 1 SDK 的月下载量已接近 5 亿次,TypeScript 与 Python SDK 各自累计下载突破 10 亿次。当一个协议以这种规模被生产环境依赖时,它的每一次演进都必须为「规模化部署」而设计——这正是本次更新的主线:MCP 正在从一个双向有状态的流式协议,变成一个请求/响应式的无状态协议。

AI 编程的现实主义:为什么是 2x 而不是 10x——兼谈重构的 Token 经济学

· 阅读需 7 分钟

2026 年 7 月底,两篇关于 AI 编程的文章先后登上 Hacker News 首页:一篇是独立开发者 obryant 的《2x, not 10x: coding with LLMs in 2026》(214 分),另一篇是 Thoughtworks CTO Giles Edwards-Alexander 在 Martin Fowler 博客发表的《The Economic Benefit of Refactoring》(194 分)。

两篇文章放在一起读,恰好构成了 2026 年 AI 编程最完整的一幅现实图景:模型能力已经跨过「可用」门槛,但生产力红利的上限不是模型智商,而是工程系统——代码组织、反馈回路、工具链——的匹配度。 本文梳理两篇的论点,并用 Thoughtworks 实验中的真实数据,量化「重构 = 投资」的 Token 经济学。

AI Agent 工程化实践:构建可观测、可管控的生产级智能体系统

· 阅读需 10 分钟

本文是 Agent 工程系列的综合篇。前文分别探讨了 Agent 推理与执行分离 和 Agent 安全防护(ToolGuardian),本篇从可观测性、安全管控与错误恢复三个维度,系统梳理生产级 Agent 系统的工程实践。

2025-2026 年,AI Agent 从「Demo 阶段」迈入「生产阶段」。Gartner 的 2026 年 Hype Cycle 报告将 Agentic AI 定位在「泡沫破裂前的过热期」——这意味着大量系统正在实际生产环境中运行,相关的工程教训也在加速积累。

本文不讨论 Agent 的理论架构,而是聚焦于三个工程问题:怎么知道 Agent 在做什么?怎么确保它不会做不该做的事?当它失败时怎么恢复?

Agent 安全防护:ToolGuardian 深度解析

· 阅读需 12 分钟

引言​

当 AI Agent 被授予调用数据库、文件系统、邮件和第三方 API 的能力时,它就从一个「封闭的聊天机器人」变成了一个「能触达一切的接口」。这把双刃剑的锋利程度,在 2025-2026 年多次安全事件中已被验证:

  • Salesforce Einstein 被注入攻击,delete_contact 接口批量删除客户记录
  • GitHub Copilot Chat 在处理 Issue 时不慎将内部 API Key 拼入工具调用参数
  • AutoGPT 第三方插件通过工具调用链获取主机系统权限

安全不再是附加功能,而是 Agent 走向生产环境的准入门槛。 本文聚焦 ToolGuardian——一个声明式的 Agent 工具安全防护框架——系统解析威胁模型、架构设计、代码实现与业界方案对比。

Agent 推理与执行分离:构建可维护的 AI 智能体架构

· 阅读需 11 分钟

面向资深后端工程师,深入 Agent 架构中的推理与执行分离模式。

从单体 Agent 的困境说起​

假设你构建了一个简单的 ReAct Agent:LLM 在同一个循环中同时完成「思考做什么」和「具体怎么做」。代码往往长这样:

while True:
thought = llm.generate(f"当前状态:{state},下一步做什么?")
action = parse_action(thought) # 执行工具调用
result = execute_tool(action)
state += f"\n执行结果:{result}"

看起来简洁,但部署到生产后会遇到一系列问题:

  • 调试困难:一个失败的步骤到底是推理错了还是执行错了?日志里 Thought/Action/Observation 混在一起,难以定位根因。
  • 测试困难:你想 mock 工具调用来测试推理逻辑,但推理和执行在一个函数里,只能端到端测试。
  • 上下文污染:上一次执行的冗余输出挤占了下一次推理的窗口。
  • 无法重放:生产环境出了 bug,你想重放当时的推理链路——但执行结果不可重复(API 超时、数据库状态变了)。

这些问题的根源在于:推理(Reasoning)和执行(Execution)是两种不同的关注点,却被耦合在了同一个循环中。