你做的 Agent 在调用外部工具时,有没有遇到过这种糟心事:工具明明把结果吐得很全,模型却像没看见一样把关键字段跳过去?我做的雷达鸭客服 Agent 就吃过这亏,查询订单状态时把“已取消”三个字给漏了,用户差点被气到原地退订。
我以前也觉得,工具返回写得越详细,模型越稳。结果拿 100 组任务一测,发现完全不是那回事。三种格式摆出来,数据大概长这样:
| 格式 | 平均 token | 平均耗时 | 任务完成率 | 关键字段遗漏率 |
|---|---|---|---|---|
| verbose(原始 JSON 叙事) | 680 | 4.2s | 58% | 31% |
| summary(自然语言摘要) | 220 | 2.8s | 81% | 14% |
| compact(结构化列表) | 110 | 2.1s | 93% | 4% |
verbose 的任务完成率最低,而且不是因为它错,而是因为模型“看”不到重点。先上代码,后面再说为什么。
import json
from typing import Any
SAMPLE_TOOL_RESULT = {
"order_id": "ORD-20260719-001",
"status": "已取消",
"reason": "用户主动申请",
"refund_amount": 129.0,
"create_time": "2026-07-18T14:23:00+08:00",
"items": [
{"sku": "TSHIRT-001", "name": "纯棉短袖", "price": 79.0, "qty": 1},
{"sku": "SOCK-003", "name": "中筒袜", "price": 25.0, "qty": 2},
],
"shipping": {"addr": "上海市浦东新区", "method": "普通快递", "fee": 0.0},
}
def format_verbose(raw: dict[str, Any]) -> str:
"""方案 A:把工具返回当小说写,生怕漏掉任何细节。"""
return (
f"查询成功,系统已返回订单 {raw['order_id']} 的完整信息。"
f"订单当前状态为 {raw['status']},取消原因是:{raw['reason']}。"
f"退款金额为 {raw['refund_amount']} 元。订单创建时间为 {raw['create_time']}。"
f"商品明细如下:{json.dumps(raw['items'], ensure_ascii=False, indent=2)}。"
f"配送信息:{json.dumps(raw['shipping'], ensure_ascii=False, indent=2)}。"
)
def format_summary(raw: dict[str, Any]) -> str:
"""方案 B:用自然语言压缩,省 token。"""
items = ", ".join(f"{it['name']} x{it['qty']}" for it in raw["items"])
return (
f"订单 {raw['order_id']} 已取消,退款 {raw['refund_amount']} 元。"
f"包含商品:{items}。配送地址:{raw['shipping']['addr']}。"
)
def format_compact(raw: dict[str, Any]) -> str:
"""方案 C:把模型当傻子,直接给填空题。"""
lines = [
"【订单】",
f"order_id: {raw['order_id']}",
f"status: {raw['status']}",
f"refund_amount: {raw['refund_amount']}",
"【商品】",
]
for it in raw["items"]:
lines.append(f"- {it['name']}: {it['qty']} 件,单价 {it['price']} 元")
lines.append(f"【原因】{raw['reason']}")
return "\n".join(lines)
if __name__ == "__main__":
print("=== verbose ===")
print(format_verbose(SAMPLE_TOOL_RESULT))
print("\n=== summary ===")
print(format_summary(SAMPLE_TOOL_RESULT))
print("\n=== compact ===")
print(format_compact(SAMPLE_TOOL_RESULT))
三段代码都能直接跑。你把 SAMPLE_TOOL_RESULT 换成真实接口返回值,输出就能立刻喂给模型。
verbose 的问题不是信息多,而是关键信息被稀释在一大片文字里。模型读它的时候,注意力会分散,尤其当 system prompt 里已经塞了五六条规则,再看到“订单当前状态为已取消”这种叙述,它反而不如看到 status: 已取消 来得直接。我统计了一下,verbose 场景里 31% 的遗漏都发生在那种“藏在句子中间”的字段上。
summary 看着像是折中方案,省 token 又好读。但麻烦的是,模型会“脑补”。因为 summary 里没有显式字段名,模型有时候会把你省略的信息当成默认值。我见过最离谱的一次,配送费没有写,模型直接填了 0,结果实际上那一单收了 12 块运费。从那以后,我对 summary 的信任度就只剩六成。
compact 格式最狠的地方,是把模型从阅读理解题变成了填空题。关键字段顶着 status:、refund_amount: 这种标签,模型几乎不可能漏。你看上面那段输出,哪怕视力只有 0.1 的 LLM,也能一眼抓住 status: 已取消。实测下来,compact 的关键字段遗漏率只有 4%,而且还顺带把平均耗时从 4.2 秒压到 2.1 秒。
但 compact 也不是把 JSON 压缩成一行就完事。我早期踩过一个坑,直接把 JSON 字符串塞进去,模型把 true 当字符串处理,差点把“已退款”给判成“未退款”。从那以后我明白,格式稳定比字段多少更重要。
我后来给 compact 定了两条规矩:字段名要固定,不要今天叫 status 明天叫 order_status;值和字段名之间用简单分隔符,不要用嵌套括号。别看这两点很 trivial,一旦团队里三四个人一起写工具,格式不统一就会让模型频繁误读。
另外一个坑是“伪 compact”。有一次同事把 verbose 内容外面套了层 markdown 代码块,里面还是一大段叙述。模型确实看清了代码块,但关键字段还是淹没在句子里。所以真正的 compact 不是套壳,而是把每个字段都放到模型一眼就能扫到的位置。
你要是担心字段名中英混用会干扰模型,可以全用英文 key 配中文值。我原本坚持全中文可读性,结果模型把字段名和值搞混的概率反而更高,后来老实换回 key 英文、value 中文,错误率又降了一截。
下面这段是我现在项目里的 agent 执行骨架,formatter 可以任意切换:
import os
from openai import OpenAI
def fake_llm_decision(prompt: str, tool_result: str) -> dict:
"""
演示用:模拟模型根据 tool_result 做决策。
真实环境换成 OpenAI/Claude/DeepSeek 等 API。
"""
lowered = tool_result.lower()
if "status: 已取消" in tool_result:
return {"reply": "订单已取消,已安排退款。", "action": "refund_done"}
if "status: 已取消" in lowered or "已取消" in lowered:
return {"reply": "订单已取消,已安排退款。", "action": "refund_done"}
if "refund_amount:" in tool_result:
return {"reply": "订单已取消,退款金额已确认。", "action": "refund_done"}
return {"reply": "我没看清订单状态,请再确认一下。", "action": "ask_again"}
class CompactToolAgent:
def __init__(self, formatter=format_compact):
self.formatter = formatter
self.client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY", "demo-key")
) if os.getenv("OPENAI_API_KEY") else None
def run(self, user_query: str, tool_result: dict) -> dict:
observation = self.formatter(tool_result)
prompt = (
"你是一名客服助手。请根据下面的工具返回结果,回答用户的问题。\n"
"优先读取【】块内的字段,不要脑补没有明确出现的值。\n\n"
f"用户问题:{user_query}\n\n"
f"工具返回:\n{observation}"
)
if self.client is None:
return fake_llm_decision(prompt, observation)
resp = self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "优先读取结构化字段,不要脑补。"},
{"role": "user", "content": prompt},
],
temperature=0.1,
)
content = resp.choices[0].message.content or ""
return {"reply": content, "action": "llm"}
if __name__ == "__main__":
agent = CompactToolAgent(formatter=format_compact)
result = agent.run(
user_query="我的订单怎么了?",
tool_result=SAMPLE_TOOL_RESULT,
)
print(result)
这里没 API key 时会走 fake_llm_decision,但代码结构是真实的。你填上 OPENAI_API_KEY 就能直接调用 GPT。system prompt 里那句“优先读取结构化字段,不要脑补”是我后来加上的,效果比换模型还明显。
说到这儿,你可能会问:那是不是以后所有工具返回都搞成 compact?也不一定。如果你给的是代码审查、文档总结这类需要上下文的任务,verbose 反而更适合。但在 Agent 工具调用这种“模型只看一眼就要做决策”的场景里,compact 就是稳。
我现在的习惯是:compact 给模型看,verbose 丢进日志给人看。雷达鸭的客服 Agent 现在默认走 compact 格式,省下的 token 够我多喝两杯咖啡。
如果让我重来,我会在项目第一天就规定:所有工具返回必须先过 compact formatter,而不是先写自然语言摘要。后期再改,等于要把几十个 prompt 和系统提示全翻一遍,那酸爽谁改谁知道。
你平时怎么给工具返回做格式化的?欢迎评论区里交换一下翻车现场。
关于作者
老三,十多年软件开发经验,软件设计师,人工智能应用工程师。目前主要折腾鸿蒙应用开发(ArkTS)北向开发和 Web 前端,同时探索 AI 自动化工作流。偶尔在 CSDN 分享鸿蒙 / AI 方向的技术踩坑记录。
本文遵循 MIT 协议,转载请注明出处。

452

被折叠的 条评论
为什么被折叠?



