主题
第 4 章 · 工具描述是给模型看的 prompt
pixiu 锚点:
agent/tools.py(32 工具的真实描述写法)、_exec_*错误返回 关键案例:13c66d6f——agent 误操作弄坏用户持仓 →manage_portfolio降权只读
开篇:一个血的教训
先讲一个让我改设计的事。
pixiu 早期,manage_portfolio 工具是能执行买卖的——agent 可以直接在对话里帮用户买入卖出。直到有一次,agent 在一次操作里,把用户真实持仓的成本价和持股数搞乱了。海康威视、中国中免,成本和股数全错乱。
这不是查错一个数据的问题——这是真金白银。查数据查错了,重查就行;持仓数据错了,用户的整个盈亏计算、止损判断、净值曲线全跟着错。资金类操作出错,后果比查询类操作严重一个数量级。
那次事故之后,我做了一个干脆的决定,登记在对话 13c66d6f 的关键决策里:
交易买卖操作从 Agent 降权为页面功能:买卖/建仓走页面表单 + 二次确认弹窗,Agent 降级为只读"查询+分析"角色。原因是 agent 误操作弄坏了用户真实持仓,资金操作出错后果远比查数据严重。
现在你去看 tools.py 里的 manage_portfolio,它的执行器长这样:
python
def _exec_manage_portfolio(args: dict) -> str:
return (
"当前为只读模式,无法执行买入、卖出、止损止盈或创建组合操作。"
"请在持仓页面手动操作交易。如需查看持仓信息,请使用 get_portfolio 工具。"
)而且它的描述里就写明了:"买入、卖出、止损止盈设置请在持仓页面手动操作,不要调用此工具。"——在工具描述层面就拦住模型去调。
这个教训引出了本章的核心:工具不只是"能干什么",还有"该不该让 agent 干"。 而这一切,都体现在一个被严重低估的地方——工具描述。
核心命题:工具描述 = 给模型的 prompt
传统程序员把 API 文档当"给人看的说明"。到了 agent 工程,你要换个脑子:工具描述是给模型看的 prompt。工具描述里漏一句话,效果等同于 system prompt 里漏一句话。
这不是夸张。模型决定"调哪个工具、传什么参数",完全依赖工具的 description 字段。你描述写得含糊,模型就瞎调;你写得精准,模型就稳。
pixiu 的 tools.py 里 32 个工具,每一个 description 都是按"给模型看"的标准写的。我挑几种最有代表性的写法给你看。
写法一:写明"不适用场景"
get_market_data 的描述:
python
"description": (
"获取标的的日线行情数据(OHLCV)。需要先通过 download_data 下载过数据。"
"不适用:查询是否交易日(用get_trading_info)、查分红(用get_dividend_data)。"
"示例:用户问'中国神华最近走势' → {\"symbol\": \"601088\", ...}"
)重点是中间那句"不适用:查询是否交易日(用 get_trading_info)、查分红(用 get_dividend_data)"。
为什么要写这个?因为模型很容易把"查行情"和"查分红""查交易日"搞混——它们都是"查标的数据"。你不主动划清边界,模型就会用 get_market_data 去查分红,拿不到结果,再换工具,来回折腾。一句"不适用"提前堵死了这种误用。
这就是把工具描述当 prompt 写的第一个标志:不只写"我能干什么",还要写"我不该干什么"。
写法二:写 few-shot 示例
注意上面那个描述里还有一句:
示例:用户问'中国神华最近走势' → {"symbol": "601088", ...}这是 few-shot 示例——直接给模型看"用户怎么问 → 你怎么调"。模型照着填参数就行,几乎不会出错。
pixiu 里凡是有"自然语言到参数"转换的工具,基本都带这种示例。download_data、run_backtest、compare_strategies 全有。这一招成本极低、收益极高。
写法三:写负面指令(防卡死)
download_data 的描述里有这么一句:
⚠️ 网络错误时不要反复重试下载不同标的。如果一次下载失败,改用 search_news 获取信息。这是负面指令——直接告诉模型"别这么干"。
为什么需要它?因为 Tushare 数据源会限频,一旦触发限频,模型很容易陷入"下载失败 → 换个标的再试 → 又失败 → 再换"的死循环。这句负面指令,配合代码层的限频冷却(_download_cooldowns,触发限频后冷却 1 小时),从描述和代码两层把这种卡死行为堵住。
负面指令是踩坑踩出来的。 你不会一开始就写"别反复重试"——你是被模型的死循环坑过几次,才把这句话加进描述里。好的工具描述,就是这么从错误里长出来的。
写法四:错误信息要"可操作"
工具描述之外,工具的错误返回是第二个被低估的地方。pixiu 的所有错误返回,都遵循一个原则:不只说"错了",还要说"下一步干什么"。
对比两种错误返回:
❌ 坏的:"参数错误"
✅ 好的:"缺少必填参数 'symbol'。请提供 6 位股票/ETF 代码,如 '601088'、'510300'。"❌ 坏的:"无数据"
✅ 好的:"标的 601088 无行情数据。请先调用 download_data 下载该标的数据
(symbol='601088', start_date='起始日期')。"注意好的错误信息做了三件事:说清缺什么(symbol)、给正确格式(6 位代码)、指下一步(调 download_data,还把参数模板给你)。
模型拿到这种错误信息,能自己修正重试——它知道下一步该调什么、参数怎么填。而拿到"参数错误"三个字,模型只能瞎猜。这个差别,就是"错误可操作"和"错误不可操作"的差别,直接决定 agent 能不能自愈。
pixiu 的 execute_tool 兜底(tools.py:1745)把这个原则贯彻到了每一层:参数解析失败、未知工具、异常捕获,每个分支返回的都是"可操作的错误文本",而不是抛个 stack trace 让模型懵圈。
写法五:工具边界——什么不该做成工具
回到开头的血泪教训。工具设计的第五层,是主动收权。
manage_portfolio 从"能买卖"变成"只读",是一个典型的工具边界设计。原则是:
有副作用、后果严重、不可逆的操作,别给 agent 全权。要么做成只读,要么要求二次确认。
pixiu 的买卖操作最后走的是"页面表单 + 二次确认弹窗"——把决策权交回给人。agent 只负责"查询 + 分析",不碰"执行"。这是一个重要的分工:agent 擅长的是信息处理和判断,不是不可逆的执行。 把执行权留给人,是给系统加一道刹车。
你设计自己的 agent 时,要想清楚:哪些工具是"只读无害"的(随便 agent 调),哪些是"有副作用"的(发邮件、改数据、花钱、删东西)——后者要么收权,要么加确认。
工具数量:32 个多不多?
pixiu 现在有 32 个工具。经常有人问:工具多了会不会让模型选晕?
我的经验是:数量不是问题,职责清晰才是。 32 个工具之所以没让模型晕,是因为每个都有明确的、不可替代的职责,而且描述把"什么场景用谁"写清楚了(见上一章的粒度分层)。真正让模型晕的,是几个工具职责重叠、描述含糊——那才是灾难。
当然,工具也不是越多越好。每加一个工具,都要问:它和现有工具的边界在哪?能不能用现有工具组合实现?pixiu 的工具是随着 W3-1/3/5 这种"发现缺口 → 补工具 → eval 验证"的过程长出来的,不是一次性堆上去的。
这一章的工具:工具描述自检
拿你的 agent 工具,过一遍这五条:
- [ ] 不适用场景:描述里有没有写"这个工具不该用来干什么"?
- [ ] few-shot 示例:自然语言到参数的工具,有没有给"用户怎么问 → 你怎么调"的示例?
- [ ] 负面指令:有没有踩过坑之后加的"别这么干"?
- [ ] 可操作错误:错误返回有没有"缺什么 + 正确格式 + 下一步调什么"?
- [ ] 工具边界:有副作用的操作,收权了吗?还是 agent 想干啥干啥?
五条都过关的工具,调用准确率会从"勉强能用"变成"稳定可靠"。这不是玄学,是 pixiu 的 32 个工具实测出来的。
小结
工具设计,本质上是在做两件事:给模型足够的能力(颗粒度分层,第 3 章),给模型清晰的边界(描述写法 + 错误可操作 + 收权,本章)。
而且——这一章对你应该有种熟悉感。接口设计、错误处理、边界划分、防越权,这些全是传统软件工程的看家本领。agent 时代的 harness 工程,换了一批新约束(描述是给模型看的、错误要可操作、执行要收权),但内核还是你熟悉的那套。这是传统程序员的主场,别被"AI 工程"四个字吓到。
下一章,我们往上走一层——context。当工具和 prompt 都搞定,决定 agent 聪明与否的,是它决策时能看到什么。
下一章
第 5 章 · 上下文:给模型正确的信息 —— 一个 eval 把 pass_rate 从 20% 做到 100% 的故事。