我给 Agent 的工具返回写了 800 字,结果它把关键信息吃了——三种格式实测

你做的 Agent 在调用外部工具时,有没有遇到过这种糟心事:工具明明把结果吐得很全,模型却像没看见一样把关键字段跳过去?我做的雷达鸭客服 Agent 就吃过这亏,查询订单状态时把“已取消”三个字给漏了,用户差点被气到原地退订。

我以前也觉得,工具返回写得越详细,模型越稳。结果拿 100 组任务一测,发现完全不是那回事。三种格式摆出来,数据大概长这样:

格式平均 token平均耗时任务完成率关键字段遗漏率
verbose(原始 JSON 叙事)6804.2s58%31%
summary(自然语言摘要)2202.8s81%14%
compact(结构化列表)1102.1s93%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 协议,转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值