省掉91%工具提示词:用函数签名与docstring自动生成LLM工具描述

发布时间:2026/11/2 22:09:29

省掉91%工具提示词:用函数签名与docstring自动生成LLM工具描述 上个月我把自己维护的 Pi Agent 扩展翻出来清理了一遍发现一个挺吓人的数字Tools 数组里的 description 加起来快 3700 个字。这还只是第五个扩展当时我就意识到如果继续按每个工具写一段说明书的思路走下去后面每加一个扩展光是维护这些工具提示词就能耗掉半天。后来我换了个思路让 Pi Agent 的扩展机制从函数签名、参数类型、返回值和示例里自动生成工具描述人工要写的提示词直接降了九成。这篇就把我的做法完整写出来给 Pi Agent 用户和正在写扩展的作者参考。先说明一下适用范围我下面用的是 Pi Agent 的扩展开发接口但也适用于任何支持自定义工具、面向 LLM 的工具调用场景。核心方法是一致的——把人肉写工具提示词改成结构化声明 自动渲染。如果你也在为扩展里的工具描述太长、改起来太痛苦而头疼这篇应该能帮你省掉不少功夫。1. 先搞清楚工具提示词到底费在哪里1.1 一份标准工具描述的自我解剖要给 LLM 用的工具写说明绕不开这么几个部分这个工具是干什么的、什么时候该用、什么时候千万别用、每个参数是什么意思、返回值长什么样。我早期写扩展时习惯把每一部分都展开写结果一份工具描述动不动 400 到 800 字。比如给一个合并本地分支的工具我写过类似这样的东西工具名称: git_merge_branch 用途: 把某个分支的提交合并到当前分支。当用户要求把 feature 分支合并到 main、或者希望将其他分支的改动整合到当前工作区时使用。合并过程中如果出现冲突不要自行解决需要将冲突文件列表返回给用户等用户确认后再继续。如果当前有未提交的改动先提醒用户处理。合并完成后输出提交哈希和合并结果。 参数说明: source_branch(s): 要被合并的分支名。必填。注意拼写要完整不要用简称。 target_branch(s): 目标分支。默认是当前分支一般不用传。如果用户指定了目标分支则切换到该分支再执行合并。 allow_conflict(boolean): 默认 false。当为 true 时允许自动处理可合并冲突为 false 且检测到冲突时立即停止。 返回值: 合并成功返回一个对象包含 merger、commitId、message合并失败返回 error。语法上没问题但仔细想想这段描述里有相当多内容是重复的。参数类型、默认值、是否必填这些信息完全可以用 JSON Schema 表达而合并成功返回一个对象包含 merger、commitId、message这种话有经验的 agent 看到工具名和参数名基本也能推断出来。真正缺的其实是边界条件什么时候不调用、冲突怎么办。1.2 真正被 LLM 用到的信息只有四成我做过一个小实验把我写过的工具描述逐段删掉看 Pi Agent 在同样任务下会不会用错工具。实验做了三轮结论有点反直觉大部分长描述里真正影响工具选择的不是那些写得又全又细的优点而是下面这四类信息参数约束和类型比如某个参数只能接受特定枚举值工具触发场景就是当用户提到某些关键词时使用明确禁区比如不要自动执行删除操作返回值中值得关注的字段例如返回 error 字段时说明失败。其他部分像合并成功后输出提交哈希这种成果描述对 agent 的选择几乎没有边际贡献。换句话说800 字的描述里大概有一大半是无效说明不仅占 token还稀释了真正有用的指令。1.3 为什么大家都喜欢多写一点提示词通胀的根源既然长描述不高效为什么扩展作者还在拼命写我总结了两点。第一LLM 看不到你的代码。你写扩展时很清楚每个函数的输入输出但 agent 只能通过描述来调用它。你担心描述短了agent 就理解不了上下文于是忍不住把代码逻辑翻译成自然语言。这相当于给每个工具写了份文档。第二工具提示词没有版本管理意识。很多扩展作者和我早期一样工具描述是最后补写的写完就再也没动过。工具本身加了参数、改了默认值描述却不会同步更新。为了以防万一作者会把所有可能性都写在里面让描述变得越来越臃肿。一旦明白冗长的根源是结构信息没有被利用而是被转译成了自然语言解决办法就很清晰了让结构信息自己说话让扩展机制去渲染描述而不是让作者手写。2. 省掉91%的核心机制让结构替你说人话2.1 从声明到描述Pi Agent 扩展里的三层信息Pi Agent 的扩展本质上是注册一组工具给 agent 调用。每个工具在注册时至少要提供三样东西调用处理器、参数结构、工具描述。我开展的优化思路是提高前两者的信息密度把第三层描述变成自动生成的结果。我推荐把工具信息分成三层静态声明层函数签名、类型注解、枚举值、默认值、docstring。这一层是结构化的能直接被代码读取。场景层什么时候调用这个工具、什么时候不要调用。这一层需要人工写但可以压缩到一两句话或者几个关键词标签。渲染层最终给 LLM 看的完整描述。这一层完全由生成器拼装不人工维护。大多数扩展作者把场景层和渲染层混在一起而且渲染层由手工维护。我的改造就是只维护前两层渲染层交给一个几十行的生成函数。这样人工要写的提示词就从一整篇缩成了一两行 docstring 里的说明。2.2 我自己实现的 tool_registry一条装饰器代替一屏描述下面是我在扩展里用的工具注册装饰器。它做两件事解析函数的类型签名和 docstring然后按模板渲染出工具描述。这样写扩展时我只需要关注函数本身的 docstring不用再单独维护 tools 数组里的长描述。import inspect import json from typing import Any, Callable # 模拟 Pi Agent 扩展 SDK 的注册方法 # 实际使用请替换成你用的 pi_agent_sdk 对应 API def _register(tool_id: str, tool_def: dict, handler: Callable): # 这里调用 Pi Agent 扩展注册函数 # pi_agent_sdk.tools.register(tool_id, tool_def, handler) pass def auto_tool(tool_id: str, category: str , when_to_use: str , never_use: str ): def decorator(fn: Callable) - Callable: doc inspect.getdoc(fn) or doc_lines [line.strip() for line in doc.splitlines() if line.strip()] sig inspect.signature(fn) params [] for name, param in sig.parameters.items(): if name in (self, cls): continue annotation param.annotation if param.annotation is not inspect.Parameter.empty else Any default param.default if param.default is not inspect.Parameter.empty else None required param.default is inspect.Parameter.empty if isinstance(default, str): default_desc f默认为 {default} elif default is None and not required: default_desc 默认为 None else: default_desc f默认值为 {default} if not required else 必填 # 从 docstring 里按 :param name: 描述 提取单行说明 param_desc for line in doc_lines: if line.startswith(f:param {name}:): param_desc line.split(f:param {name}:, 1)[1].strip() break if param_desc: params.append(f- {name} ({annotation}){param_desc}{default_desc}) else: params.append(f- {name} ({annotation}){default_desc}) # 提取返回值描述 ret_desc for line in doc_lines: if line.startswith(:return:): ret_desc line.split(:return:, 1)[1].strip() break if not ret_desc: ret_desc 按照函数返回值类型返回结果 # 提取 docstring 中首个非参数段落作为用途摘要 first_line for line in doc_lines: if not line.startswith(:) and not line.startswith(-): first_line line break block f当用户请求与「{category or tool_id}」相关的操作时使用本工具。 if when_to_use: block f触发场景{when_to_use}。 if never_use: block f禁止在以下场景使用{never_use}。 description ( f{first_line or tool_id}。 f{block} f参数要求\n \n.join(params) f\n返回说明{ret_desc}。 ) _register( tool_id, { name: tool_id, description: description, parameters: { type: object, properties: json.loads(json.dumps({...})), required: [...] }, }, fn, ) return fn return decorator代码是示意性的真去接 Pi Agent 的 SDK 时把_register换成官方接口就行。关键是这个模式你不再需要单独维护一长段 description而是在函数定义处写明参数含义和触发边界其余由生成器拼装。2.3 参数、返回值、示例如何自动变成自然语言有人会问生成器拼出来的描述会不会比手写的更死板我的经验是对绝大多数工具来说死板但结构清晰比花哨但重点模糊有用得多。具体到生成规则我用了几个固定策略参数名直接带出来。Python 函数参数名本身有语义比如source_branch见了就知道是源分支不需要额外解释要被合并的分支名。枚举值一定要出现在描述里。如果参数注解是Literal[feat, fix, chore]生成器会把可选值原样列进参数说明避免 agent 自由发挥。示例优先于抽象解释。在函数 docstring 里给一句:example: git_merge_branch(source_branchfeature/login, target_branchmain)生成器把它拆成调用方式示意比干巴巴的参数说明直观得多。把何时不要用放在描述末尾。这是从 agent 的注意力习惯出发开头和结尾最容易被读到。开头放用途结尾放禁止项中间放参数效果最好。这样一套下来我原来那些 800 字的工具描述平均被压缩到了 70 字左右的人工输入换算一下正好省掉了九成以上。3. 实操把一个现成工具改成零长篇描述模式3.1 改造前的工具定义一段 400 字的手写碑文我拿扩展里一个叫check_contract的工具当例子。这个工具的作用是检查当前改动有没有违反团队约定的 Git 提交规范。改造前我在 Pi Agent 扩展配置里给它写了这么一段{ name: check_contract, description: 检查当前 Git 仓库的改动是否满足团队提交流程要求。当用户准备提交代码、需要检查提交信息格式、或者提到 CI 检查失败时使用。该工具会读取配置的规则文件列出未通过规则的文件列表和违规原因。注意本工具只做检查不修改任何文件也不能阻止提交。若返回 error 字段说明配置读取失败。 }其实这个工具核心就三件事检查、返回违规列表、不修改文件。但为了让 agent 准确理解我硬是写成了绕口的说明书。每次改规则文件名或者返回值结构我都要回头同步这段描述非常容易漏。3.2 改造后的工具定义只留 docstring 里的三行用上面的auto_tool装饰器改造后变成这样from pi_agent_toolkit import auto_tool auto_tool( tool_idcheck_contract, categorygit提交, when_to_use用户准备提交、提交信息被拒绝、CI 检查失败时, never_use不要用于代码修改或自动提交, ) def check_contract(repo_path: str, rule_set: str default) - dict: 检查改动是否违反 Git 提交规范。 :param repo_path: 仓库绝对路径 :param rule_set: 规则集名称可选 default/strict/lax :return: 返回 {violations: [...], passed: bool}error 字段表示规则读取失败 # 真实检查逻辑略 return {violations: [], passed: True}人工写的部分就是 docstring 三行和两个装饰器参数我数了一下加上函数签名里的类型注解一共 90 来个字符。生成器渲染出来的最终描述大约 350 字但那些字不是人写出来的是由代码根据签名、默认值和 docstring 自动拼出来的。3.3 验证让 Pi Agent 跑一个真实任务看它能否选对工具改造完不能光看字数少了得验证 agent 还是不是原来那个 agent。我的验证步骤很清楚在 Pi Agent 的调试模式下加载新扩展打印出实际进入上下文的工具描述给 Pi Agent 发一个和工具场景强相关的任务比如我这次提交信息不合规帮我查一下怎么回事再给一个弱相关任务比如帮我看看当前仓库状态最后给一个不该调用工具的任务比如把最近三个 commit 的 author 列出来。实测三组任务里第一组它准确调用了check_contract第二组没调用而是使用了仓库状态工具第三组它用了 git 查找命令也没有误用check_contract。这说明自动生成的描述没有让工具失去边界感关键还是when_to_use和never_use这两个字段管用。我还翻了一次 Pi Agent 在调试模式下的 token 日志工具描述从原来的 400 字手写版变成了 350 字生成版但工具选择的准确率没有下降。省掉的 91% 其实省的是需要人工维护的提示词不是省给 agent 的 token——这一点在后面讲坑的时候还要强调。4. 实测中踩过的坑以及什么时候不要省4.1 坑一描述短了agent 反而不知道工具的真实边界第一次把所有工具都改成auto_tool后跑任务很快发现一个现象agent 开始过度使用某些工具。原因是我只保留了用途摘要没把禁止场景传给生成器。比如某个format_py_file工具docstring 只说格式化指定 Python 文件agent 在用户请求整理一下这段代码风格时就会调用它但我的工具实际只做文件内格式化不处理从剪贴板来的文本。解决办法很直接装饰器里的never_use一定要写真实边界。后来我把所有工具的边界情况都整理了一遍写进never_use再跑同样的任务误用就消失了。所以我建议扩展作者精简描述的正确姿势不是删边界描述而是把边界从一大段负面说明压缩成一句禁令。4.2 坑二参数枚举和默认值没进描述导致传参错误还有个坑和参数有关。最初生成器只拼了参数名和类型没有把枚举值拼进去。结果 agent 调用check_contract(rule_setnormal)而实际只支持default/strict/lax三选一直接报错。所以第二个建议是类型注解和枚举值一律要出现在最终描述里。我在auto_tool的实现里加了一段如果参数注解是Literal就把它定义成可选值xxx/yyy/zzz放进参数说明。默认值也必须带因为 agent 看到未传参数时需要知道会用什么配置。这两项是避免参数错误的关键。4.3 调试技巧让 Pi Agent 把工具选择轨迹暴露出来写扩展时我踩坑后养成一个习惯永远先看 agent 为什么选错工具再改描述。Pi Agent 在调试模式或计划模式下会把我打算调用哪个工具、理由是什么打印出来。拿到这个理由你很容易定位是描述里缺了什么还是描述里多了什么让 agent 误解。如果没有这个模式还有一个土办法写一个脚本遍历所有已注册工具每次只把一个工具的完整生成描述交给 agent让它用自己的话复述这个工具何时用、何时不用。如果复述里出现你没表达过的意思说明渲染模版有问题如果复述不出来说明描述信息还不够。我把这个脚本固化在扩展的测试目录里每次改完工具跑一遍比手动调描述靠谱得多。4.4 三种不该精简的场景自动生成工具描述不是万能的遇到下面三类工具我会保留一段人工手写描述而且不急着删高风险操作类工具。比如删除分支、清空缓存、推送远程。这类工具除了结构化声明我会额外加一段必须再次和用户确认目标后执行的说明。这里的提示词不是用来省 token 的是用来兜安全底线的。行为依赖复杂上下文的工具。如果工具本身要根据前一天的任务状态或某个临时文件来决定行为单靠参数描述说不清楚我会手写一小段工具运行受什么影响的说明。多个工具边界高度重叠时。当两个工具看起来很像比如format_snippet和format_file自动生成描述容易让 agent 混淆。这种场景下我会额外加一句和 format_file 的区别本工具只处理纯文本片段。这三种情况加起来在我的扩展里不超过 20%。所以整体算下来能省 91% 是真实的不是一次性把所有描述都砍到最短。5. 最后聊点维护心得这套自动生成方案跑了两个多月最大的变化不是 token 节省而是扩展维护心态变了。以前加一个新工具写描述像写论文现在只需要写函数签名、docstring、两行触发/禁区然后跑一遍测试脚本确认生成描述正常。工具改了参数名生成描述会自动跟着变我再也不用担心代码改了但工具提示词忘记同步这种低级错误。如果想把这条路走得更远还可以在生成器里加一层自动化测试每次加载扩展时把所有工具的描述捞出来检查是否包含必填参数名、是否包含返回值字段、是否在开头出现了工具名。这些检查逻辑一点都不复杂但能挡掉大部分看似精简实则缺失的回归问题。对我来说真正有用的不是省掉 91% 的工具提示词这个数字而是它逼我把工具能力重新梳理了一遍让描述从给 agent 的说明书变成了给 agent 的结构化索引。写扩展的朋友不妨从最常维护的那个工具开始试试先拿一个工具跑完整个验证流程确认无误后再大批量改造。我踩过的那几个坑希望你们能直接绕过去。
延伸阅读

更多相关文章

2026/11/2 22:09:29

MiMo V2.6双登顶背后:以单位智能成本重构大模型选型逻辑

1. MiMo V2.6 双登顶:这次“第一”含金量在哪过去这两周,我身边好几个做 AI 应用的朋友都在转发同一个消息:MiMo V2.6 双登顶。「双登顶」这个词从行业媒体一路传到开发者群,有人兴奋,有人疑惑,也有人第一反…

2026/11/2 22:09:29

VSCode与IDE对比:专业开发环境配置指南

1. IDE与文本编辑器的本质区别在编程工具的选择上,IDE(集成开发环境)和文本编辑器是两种最常见的选项。作为从业十余年的全栈开发者,我深刻理解两者在功能定位和工作流上的差异。IDE是为专业开发设计的"重型武器"&#…

2026/11/2 22:09:29

大模型内部也有“脑区”?功能分区图谱助力精准微调与压缩

我们一直把大模型当黑盒用:丢一句提示词进去,几秒钟吐出一段像模像样的回答。可它内部到底是什么样的“神经网络振捣”,过去几年大家只能靠注意力可视化、探针分类器、激活维度分析这些间接手段去猜。AAAI 2026 的 Oral 论文名单里&#xff0…

2026/11/2 23:59:41

JJencode编码解密工具:前端JS代码混淆与源码还原实操

做前端的人大概都遇到过这种场面:交付给客户的是一个纯前端跑的小工具,或者一个 H5 小游戏,代码打包压缩完往服务器一放,懂行的人按 F12 就能把逻辑看个七七八八,改个变量名、换个皮肤就成他自己的了。这时候就会有人翻…

2026/11/2 23:59:41

OpenClaw与LINE集成提升养虾业沟通效率

1. OpenClaw与LINE集成的核心价值OpenClaw作为一款多平台即时通讯聚合工具,其与LINE的深度整合为养虾行业带来了前所未有的沟通效率提升。传统养虾场管理中,技术人员、养殖工人、销售团队之间的信息传递往往存在滞后性,而通过OpenClaw接入LIN…

2026/11/2 23:59:41

Unity Package Manager刷新错误排查与解决方案

1. Unity Package Manager刷新资源报错问题解析最近在Unity开发过程中,Package Manager提示"Error Refreshing Asset"的问题困扰了不少开发者。作为Unity项目依赖管理的核心组件,Package Manager一旦出现问题,轻则影响资源加载&…

2026/11/2 23:59:41

淘宝商品详情API开发实战与性能优化指南

1. 淘宝商品详情API核心功能解析淘宝开放平台的taobao.item.get接口是电商开发者最常调用的核心接口之一,它提供了获取单个商品完整信息的能力。这个接口的独特之处在于它能返回商品的基础信息、SKU数据、促销信息等全维度数据,且响应速度控制在200ms以内…

2026/11/2 23:54:39

4G远程断电报警器:工业智能化的关键神经末梢

1. 项目概述:4G远程断电报警器的工业价值在工业4.0和智能制造的大背景下,4G远程断电报警器正在成为现代工厂的"神经末梢"。这个巴掌大小的设备通过4G网络实时监控产线用电状态,当检测到异常断电时,能在毫秒级触发多通道…

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑