给你的 Agent 上一场“砍价考试“:用 Cattle Trade 思路搭一个最小博弈测评
TL;DR
新论文《Cattle Trade》(arXiv:2605.14537)提出一个多代理基准,在拍卖、隐藏报价、讨价还价、虚张声势里考察 LLM 在不完全信息+对抗博弈下的表现。本文不复刻整个 50–60 回合的大游戏,而是教你用约 120 行 Python 搭一个最小可运行的"砍价评测":两个 LLM 买卖一件估值对彼此隐藏的商品,看你的 agent 会不会被坑、会不会留下成交空间。文末附上《基准自审》论文(arXiv:2605.21404)给的报告清单,让你的评测可信可复现。
1. 为什么要单独测"博弈"能力
常见的 agent 评测多是"任务完成度"——能不能查对资料、跑通代码。但真实世界里,agent 越来越多地要和别的 agent 或人打交道:谈采购、抢竞价、做资源分配。这类场景的难点不在知识,而在对手会撒谎、信息不对称、且要做长链决策。
Cattle Trade 正是冲着这点设计的:它把拍卖、隐藏报价交易、讨价还价、虚张声势、对手建模和资源分配,融入一个持续 50–60 回合的长程游戏里。我们用它的"内核思想"做一个轻量版,足以暴露你 agent 的两个常见毛病:过度让步(被一句空话吓到)和寸步不让(错失本可达成的成交)。
2. 最小博弈:隐藏估值的一次性砍价
规则极简:卖家手里一件商品,成本是cost;买家对它的估值是value,且value > cost(存在成交区间)。双方都不知道对方的数字,轮流出价,谁先接受谁成交。理性结果是:只要成交价落在[cost, value]之间,双方都比谈崩好。
下面的脚本只依赖标准库 + 一个聊天补全函数。把call_llm换成你自己的模型客户端即可(OpenAI、Anthropic、本地 vLLM 都行)。
importjson,re,randomfromdataclassesimportdataclass# ===== 1. 替换成你的模型客户端 =====defcall_llm(system:str,user:str)->str:"""返回模型纯文本。这里用伪实现占位,请替换。"""raiseNotImplementedError("接上你的 chat completion 客户端")@dataclassclassDeal:cost:int# 卖家成本(卖家私有)value:int# 买家估值(买家私有)defmake_prompt(role:str,secret:int,history:list[str])->tuple[str,str]:sys=(f"你在玩一个砍价游戏,你是{role}。"f"{'你的成本是'ifrole=='卖家'else'你愿意支付的最高价是'}{secret}(对方不知道)。""每轮只能输出一行 JSON:{\"action\":\"offer\",\"price\":N} 或 ""{\"action\":\"accept\"}。不要输出其它内容。""目标:在对自己有利的前提下尽量成交,谈崩对双方都不好。")convo="\n".join(history)ifhistoryelse"(你先出价)"returnsys,f"历史出价:\n{convo}\n请给出你这一轮的动作。"defparse(text:str)->dict|None:m=re.search(r"\{.*\}",text,re.S)ifnotm:returnNonetry:returnjson.loads(m.group(0))exceptjson.JSONDecodeError:returnNonedefplay(deal:Deal,max_rounds:int=8)->dict:history,turn=[],"卖家"secret={"卖家":deal.cost,"买家":deal.value}last_price=Nonefor_inrange(max_rounds):sys,usr=make_prompt(turn,secret[turn],history)act=parse(call_llm(sys,usr))or{"action":"offer","price":secret[turn]}ifact.get("action")=="accept"andlast_priceisnotNone:return{"status":"deal","price":last_price}price=int(act.get("price",secret[turn]))last_price=price history.append(f"{turn}出价{price}")turn="买家"ifturn=="卖家"else"卖家"return{"status":"no_deal","price":None}3. 怎么打分:别只看"成没成交"
只统计成交率会骗自己——一个无脑全盘接受的 agent 成交率 100%,却是个严重亏损的"冤大头"。建议至少看三个指标:
defscore(deal:Deal,result:dict)->dict:surplus_total=deal.value-deal.cost# 总可分配剩余ifresult["status"]!="deal":return{"closed":0,"buyer_gain":0,"seller_gain":0,"efficiency":0.0}p=result["price"]in_zone=deal.cost<=p<=deal.valuereturn{"closed":1,"buyer_gain":max(deal.value-p,0),# 买家省下的钱"seller_gain":max(p-deal.cost,0),# 卖家赚到的钱"efficiency":round((surplus_total)/surplus_total,3)ifin_zoneelse0.0,"in_zone":in_zone,# 价格是否落在理性区间}defrun_suite(n:int=50,seed:int=0)->dict:random.seed(seed)rows=[]for_inrange(n):cost=random.randint(10,50)value=cost+random.randint(5,40)# 保证存在成交区间deal=Deal(cost,value)rows.append(score(deal,play(deal)))closed=sum(r["closed"]forrinrows)return{"n":n,"deal_rate":round(closed/n,3),"avg_buyer_gain":round(sum(r["buyer_gain"]forrinrows)/n,2),"avg_seller_gain":round(sum(r["seller_gain"]forrinrows)/n,2),"in_zone_rate":round(sum(r.get("in_zone",0)forrinrows)/n,3),}关注点解释:deal_rate太低说明 agent 太硬,太高(接近 1)但buyer_gain极小则说明它在被宰;in_zone_rate衡量它是否守住了理性边界。把买方和卖方都换成你的 agent 对打,再各自换成一个"诚实基线"和"虚张声势基线",就能看出它在对抗下的稳健性。
4. 让结果可信:照《基准自审》清单填表
《What Twelve LLM Agent Benchmark Papers Disclose About Themselves》(arXiv:2605.21404)审计了 12 个知名 agent 基准,发现大量评测没说清自己怎么测的。它给出的披露维度,正好可以当你自评的检查清单:
- 基准身份:版本、随机种子、样本数(上面
seed、n已固定)。 - 执行环境(harness):谁先手、最多几轮、超时与解析失败如何兜底(脚本里
max_rounds与parse失败的 fallback)。 - 推理设置:模型名、temperature、是否带工具。务必记录,否则结果不可复现。
- 成本报告:每局平均 token 与花费。
- 失败拆解:JSON 解析失败率、谈崩率分别多少,而不是只报一个总分。
把这五项连同run_suite的输出一起写进report.json,你的"砍价考试"就从玩具变成了可复现的小评测。
踩坑提示
模型经常不按指令输出 JSON——所以parse必须有兜底,且要单独统计解析失败率;它会"忘记"自己的隐藏数字而泄底,可在 system prompt 里反复强调"不要透露具体数字";轮次别设太长,否则两个 agent 会陷入 1 元 1 元挤牙膏的死循环,max_rounds=8通常够用。
注:本文为最小教学版,并非 Cattle Trade 的官方实现;要做严肃评测请以原论文与其代码为准。
参考资料
- Cattle Trade: A Multi-Agent Benchmark for LLM Bluffing, Bidding, and Bargaining — https://arxiv.org/abs/2605.14537
- What Twelve LLM Agent Benchmark Papers Disclose About Themselves — https://arxiv.org/abs/2605.21404
