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:mre.search(r\{.*\},text,re.S)ifnotm:returnNonetry:returnjson.loads(m.group(0))exceptjson.JSONDecodeError:returnNonedefplay(deal:Deal,max_rounds:int8)-dict:history,turn[],卖家secret{卖家:deal.cost,买家:deal.value}last_priceNonefor_inrange(max_rounds):sys,usrmake_prompt(turn,secret[turn],history)actparse(call_llm(sys,usr))or{action:offer,price:secret[turn]}ifact.get(action)acceptandlast_priceisnotNone:return{status:deal,price:last_price}priceint(act.get(price,secret[turn]))last_priceprice history.append(f{turn}出价{price})turn买家ifturn卖家else卖家return{status:no_deal,price:None}3. 怎么打分别只看成没成交只统计成交率会骗自己——一个无脑全盘接受的 agent 成交率 100%却是个严重亏损的冤大头。建议至少看三个指标defscore(deal:Deal,result:dict)-dict:surplus_totaldeal.value-deal.cost# 总可分配剩余ifresult[status]!deal:return{closed:0,buyer_gain:0,seller_gain:0,efficiency:0.0}presult[price]in_zonedeal.costpdeal.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:int50,seed:int0)-dict:random.seed(seed)rows[]for_inrange(n):costrandom.randint(10,50)valuecostrandom.randint(5,40)# 保证存在成交区间dealDeal(cost,value)rows.append(score(deal,play(deal)))closedsum(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_rounds8通常够用。注本文为最小教学版并非 Cattle Trade 的官方实现要做严肃评测请以原论文与其代码为准。参考资料Cattle Trade: A Multi-Agent Benchmark for LLM Bluffing, Bidding, and Bargaining — https://arxiv.org/abs/2605.14537What Twelve LLM Agent Benchmark Papers Disclose About Themselves — https://arxiv.org/abs/2605.21404
给你的 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:mre.search(r\{.*\},text,re.S)ifnotm:returnNonetry:returnjson.loads(m.group(0))exceptjson.JSONDecodeError:returnNonedefplay(deal:Deal,max_rounds:int8)-dict:history,turn[],卖家secret{卖家:deal.cost,买家:deal.value}last_priceNonefor_inrange(max_rounds):sys,usrmake_prompt(turn,secret[turn],history)actparse(call_llm(sys,usr))or{action:offer,price:secret[turn]}ifact.get(action)acceptandlast_priceisnotNone:return{status:deal,price:last_price}priceint(act.get(price,secret[turn]))last_priceprice history.append(f{turn}出价{price})turn买家ifturn卖家else卖家return{status:no_deal,price:None}3. 怎么打分别只看成没成交只统计成交率会骗自己——一个无脑全盘接受的 agent 成交率 100%却是个严重亏损的冤大头。建议至少看三个指标defscore(deal:Deal,result:dict)-dict:surplus_totaldeal.value-deal.cost# 总可分配剩余ifresult[status]!deal:return{closed:0,buyer_gain:0,seller_gain:0,efficiency:0.0}presult[price]in_zonedeal.costpdeal.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:int50,seed:int0)-dict:random.seed(seed)rows[]for_inrange(n):costrandom.randint(10,50)valuecostrandom.randint(5,40)# 保证存在成交区间dealDeal(cost,value)rows.append(score(deal,play(deal)))closedsum(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_rounds8通常够用。注本文为最小教学版并非 Cattle Trade 的官方实现要做严肃评测请以原论文与其代码为准。参考资料Cattle Trade: A Multi-Agent Benchmark for LLM Bluffing, Bidding, and Bargaining — https://arxiv.org/abs/2605.14537What Twelve LLM Agent Benchmark Papers Disclose About Themselves — https://arxiv.org/abs/2605.21404