最近不少朋友在问我同一个问题:Pi Agent 这类智能体产品,到底怎么调教才不折腾?有人还在用老办法,把工具用法、参数说明、返回格式全部塞进系统提示词,结果上下文越写越长,模型反而越来越“笨”,该调用的工具不调用,不该传的参数乱传。
其实这一行我有个很深的体会:提示词写得越多,系统越脆弱。一份真正好用的 Agent 配置,恰恰应该是“少说话、多授权”。我把自己项目里积攒的工具提示词从三十多条压缩到三条之后,实测上下文占用降了九成左右,工具调用成功率反而变高了。这就是标题里“省掉 91% 的工具提示词”的由来。
这篇文章写给两类人:一类是 Pi Agent 的普通用户,想知道为什么别人一句话就能让 Agent 自动干活;另一类是扩展作者,想搞清楚如何写一个让 Agent 一见就懂、即插即用的工具扩展。下面直接讲机制、配置和案例。
1. 先搞明白这 91% 是从哪省出来的
很多人看到“省掉 91% 工具提示词”会觉得是标题党,其实不是。关键不在于你“少写了几行字”,而在于你改变了工具描述的组织方式:从“一次性全部灌给模型”变成“按需加载、用完即走”。要理解这个转变,先看旧模式是怎么把上下文一步步拖垮的。
1.1 传统“手写工具提示词”到底输在哪
假设你现在要让 Agent 帮你查一段视频的编码格式和时长。传统做法是你在系统提示词里写:
- 你是一个视频处理助手。
- 当需要查询视频信息时,请使用工具 video_metadata。
- 该工具的第一个参数 file_path 是字符串类型,表示视频文件路径。
- 如果文件不存在,会返回 error。
- 返回 JSON 中包含 streams 和 format,注意看 codec_name。
- 如果用户询问分辨率,需要遍历 streams 中 codec_type 为 video 的流,取其 width 和 height。
- 你可能会用到 ffprobe 命令……
这还只是一个工具。当你同时挂上文件搜索、图片压缩、URL 抓取、数据清洗等十个八个工具时,系统提示词会被撑到几千 token。模型在生成回复时要从这么一大坨说明里大海捞针一样找出“当前该用哪个工具”,注意力被严重稀释。
更麻烦的是,这类提示词里的“规则”往往互相打架。比如你写过“当用户想要下载视频时调用 download_video”,另一条又写“当 input 包含 m3u8 时调用 m3u8_parser”,模型一旦遇到模糊需求,就会犹豫甚至同时调用多个工具,造成重复执行和资源浪费。
1.2 Pi Agent 扩展协议:按需加载的工具描述
Pi Agent 的做法完全不同。它引入了一套工具描述协议:每个扩展在自己的 manifest 文件里声明“我是谁、需要什么权限、能做什么、参数长什么样、返回什么结构”。Agent 在对话过程中遇到用户请求时,会先通过意图识别判断可能需要哪些工具,再按需去加载对应的描述片段,而不是在一开始就把所有工具说明都背下来。
这套机制很像我平时用的函数文档系统:IDE 里按一下快捷键,自动弹出某个函数的签名和注释,没用到之前根本不出现在屏幕里。Agent 的上下文窗口同样如此,越是精简,留给推理的空间就越充足。
举个例子。我的扩展清单里有一个 video_metadata 工具,manifest 里它的 description 只写了一句:“读取本地视频文件中视频流、音频流与封装格式的元数据,返回 JSON”。当用户说“看看这个视频是不是 H.265 编码”时,Agent 会自动把这条描述、参数 Schema 和返回说明加载进当前上下文,然后发起调用。整个过程用户无感知,也不需要自己在提示词里写“请使用 video_metadata 工具”这种废话。
1.3 为什么按需加载能省下这么多 token
做个简单的算术。假设你有 30 个工具,每个工具的完整描述(名称、触发场景、参数说明、返回结构、注意事项)平均约 120 token,一次性全部塞进系统提示词就是 3600 token。按需加载模式里,每个任务往往只涉及 1 到 3 个工具,假设平均加载 2 个,也就是 240 token,再加上一条全局入口规则约 80 token,总共 320 token。
3600 减到 320,节省幅度刚好在 91% 左右。这还没算另一个隐性收益:上下文窗口里垃圾信息变少之后,模型产生幻觉的概率明显下降,错误重试次数减少,整体响应速度也会更快。
我见过不少项目把工具提示词当成“宝贝”一直囤在系统提示词里,觉得写得多才安全。恰恰相反,真正稳定的方案是让扩展协议替你管理描述,让模型在需要时精准命中。
2. 用户侧操作:一句话让 Agent 自己选工具
理解了省 token 的原理,下面这些操作就顺理成章了。普通用户完全不需要学怎么开发扩展,只要调整自己的配置习惯和提问方式,也能把提示词压缩到极致。
2.1 用项目配置代替每轮重复的“角色+规则”
我以前常见的做法是每次会话开头都写一大段:
“你现在是一个熟悉 Python、FFmpeg、MediaInfo 的运维脚本专家。请优先使用 ffprobe 分析视频,不要使用 PIL;输出 JSON 格式,中文回答……”
这段内容每天要重复写,费时费力,而且一旦某个工具改动,你还要记得把这里的描述同步改掉。
Pi Agent 支持项目级上下文配置文件,比如pi.agent.yaml或agent.md。把技术栈偏好、输出格式、避讳项全部写进项目配置,Agent 会在对话开始时自动读取并作为背景知识。比如:
project: name: media-tools tech_stack: - python - ffprobe conventions: output_format: json language: chinese avoid: - pil以后每次打开新对话,Agent 会自动把配置里的信息加载进来,你不需要再重复“你是专家、你要用 ffprobe”这些话。配置里的内容本质上是“上下文工程”的一部分,越稳定越好,越不需要随时改动越好。
2.2 给 Agent 放权,而不是手把手指挥
很多用户不敢放权,总担心模型乱调工具。解决办法不是多写提示词,而是配置权限边界。
在 Pi Agent 的配置文件里,可以指定工具执行策略:
tools: auto_approve: false allowed: - video_metadata - url_fetch denied: - file_deleteauto_approve: false表示涉及危险操作时(比如删除、覆盖、执行任意 shell 命令),必须经过用户确认;只读类工具则自动放行。这样你可以放心地只下达任务目标,不用在提示词里反复叮嘱“小心、不要删文件、操作前先问一下我”。
放权还有一层含义:不要替 Agent 规划执行步骤。你只需要说“把当前目录下所有 mp4 的编码和分辨率汇总成表格”,剩下的查目录、遍历文件、读取元数据、格式化输出,全部交给 Agent 根据工具清单自己编排。这才能体现智能体的价值。
2.3 用目标导向提问,把提示词压缩到一句
压缩提示词的核心心法是“描述结果,不描述过程”。下面这张对比表是我实际整理过的:
| 场景 | 传统提示词 | 压缩后提示词 |
|---|---|---|
| 查视频信息 | 请先用 ls 列出当前目录,再调用 video_metadata 工具读取 xxx.mp4,然后从返回值中找到 codec_name 字段,告诉我它是不是 H.265,分辨率是多少 | 看下 xxx.mp4 是不是 H.265,顺便告诉我分辨率 |
| 抓网页标题 | 你是一个网络助手,请用 url_fetch 工具请求 https://example.com,解析 HTML,找到 title 标签里的文本,并去除前后空白 | 抓取 example.com 的页面标题 |
| 下载 m3u8 | 请用 m3u8_parser 解析链接列表,选择最高码率的分段,用下载工具拼接成 mp4,并检查文件完整性 | 把这个 m3u8 视频下载成 mp4 文件 |
可以看到,用户侧需要“省”掉的不是思考,而是那些本来由扩展协议和模型推理完成的步骤描述。你只要说清楚要什么结果,工具链的编排交给 Agent 自己去完成,它比你更清楚每一步如何调用。
3. 扩展作者侧:把工具做成 Agent 一见就懂的“说明书”
对扩展作者来说,“省掉 91% 的工具提示词”不是一个营销数字,而是一份契约:你要在 manifest 和 Schema 里把工具描述写清楚,让 Agent 不需要用户额外解释就能正确调用。下面讲我怎么设计一份合格的扩展。
3.1 manifest 的结构:入口、权限、工具声明
一份最小可用清单大概长这样:
api_version: 1 package: name: video-meta version: 0.3.2 description: 读取视频封装与流的元数据,输出标准化 JSON。 permissions: - fs.read - process.run tools: - name: video_metadata description: 读取本地视频文件的编码、分辨率、时长、码率等信息。 entry: tools/video_meta.py schema: type: object properties: file_path: type: string description: 视频文件的绝对路径或相对路径。 required: - file_path这里几个关键设计点:
- 权限列表要最小化。
fs.read和process.run已经能覆盖 ffprobe 执行和文件访问需求,就不要声明成fs.write。权限写得越宽,Agent 的审批流程越严格,反而影响效率。 entry是工具的入口文件,可以是一个 Python 脚本、一个 Node 脚本,也可以是一个命令行程序。Pi Agent 的扩展运行时负责在隔离进程中调用入口。description非常关键,它是一句话“触发条件”。我建议用“读取……返回……”这种明确句式,不要写“该工具可以用于处理大量文件”这种模糊描述。模型是通过 description 来判断什么时候该用这个工具的,写得越精准,误判越少。
3.2 参数 Schema 写得越细,Agent 试错越少
很多扩展作者的误区是:参数随便写个 string 或 object 就完事。实际上,Schema 是 Agent 生成调用参数时的唯一参考,它越严格,模型“画蛇添足”的概率越低。
举一个教训。我之前写过一个 i2c 设备读取工具,参数device_id只声明成了整数。结果 Agent 在调用时经常传0x40这种带进制前缀的字符串,导致运行时解析失败。后来我把 Schema 改成:
schema: type: object properties: device_id: type: integer minimum: 0 maximum: 127 description: 7 位 I2C 地址,范围 0 到 127,注意不要附加 0x 前缀。 examples: - 64 register: type: integer minimum: 0 maximum: 255加上examples和明确的进制说明之后,调用错误率几乎降到了零。模型在学习参数时非常依赖示例,这一点和人类去看 API 文档是一个道理。写 description 时还要避免歧义,比如“文件路径请传完整路径,相对路径基于项目根目录解析”,避免 Agent 猜。
另外一个隐藏参数是additionalProperties。如果在 Schema 里不禁止多余字段,模型偶尔会自作主张加一个它觉得有用的参数。工具运行时一旦收到未定义参数,应该直接报错,但更好的做法是在 Schema 中显式声明:
additionalProperties: false这能让模型在生成参数前就自我约束,而不是等运行时再去纠错。
3.3 返回值协议:让 Agent 能自己读懂结果
工具返回值不是给用户看的,是给模型“看”的。所以返回值必须结构化,并包含模型判断所需的状态信息。我统一用下面这个协议:
{ "ok": true, "data": { "...": "..." } }失败时:
{ "ok": false, "error": "file_not_found", "message": "文件 /tmp/a.mp4 不存在,请检查路径后再试" }ok字段让 Agent 在极短时间内判断调用是否成功;error用机器码风格,方便模型进行策略选择;message是对人类友好的说明,很多时候 Agent 会直接把它转述给用户。
不要把原始 ffprobe 输出直接丢给模型。你要在工具内部做一次解析,只保留有用的字段。比如:
def video_metadata(file_path): # 调用 ffprobe 并解析 json # 提取 duration, codec_name, width, height, bit_rate # 统一包装成 {"ok": True, "data": {...}} pass模型拿到的信息越干净,它的下一步推理就越可靠。这和“提示词工程”中的信息密度原则完全一致:上下文里只保留当前任务需要的字段,其余全部过滤。
3.4 跨平台与安全边界:扩展作者的三个坑
第一,路径处理。Windows 下反斜杠会被 JSON 转义,模型可能生成C:\Users\xx这类字符串,工具端未处理就直接报错。我的做法是工具入口统一使用os.path.abspath和os.path.normpath把传入路径规范化为正斜杠形式,并在返回错误时给出规范化后的路径示例,帮助模型自省。
第二,超时与重试。工具调用如果长时间卡住,会拖累整个 Agent 会话。建议所有外部进程调用都加超时:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)超时后在message里明确说明“命令执行超时,可能是文件过大或 ffprobe 版本问题”,让模型有机会选择降级方案。
第三,删除等危险操作必须二次确认。工具即使声明了权限,也应在执行不可逆操作前主动返回一个“确认码”,比如:
{ "ok": false, "confirmation_required": true, "confirmation_token": "a1b2c3" }只有当用户在对话里给出确认码,工具才会真正执行。这比让用户在系统提示词里写“不要删除”可靠得多。
4. 实操复现:从零写一个视频元数据扩展
前面讲了理论,这一节我完整跑一遍流程。我们做一个 Pi Agent 扩展,功能是读取本地视频文件的编码、分辨率、时长、码率等信息,对应“HEVC 视频扩展”这类常见需求场景。
4.1 准备骨架与 manifest
先建一个目录:
mkdir video-meta cd video-meta里面放两个文件:manifest.yaml和tools/video_meta.py。manifest 内容我在 3.1 节已经给出,这里再补充一个细节:工具入口声明的脚本要有可执行权限,并且首行写上#!/usr/bin/env python3,确保扩展运行环境能直接调用。
如果你开发的是 Unity 编辑器工具、浏览器插件或其他语言的扩展,原理完全一样,只是entry指向的文件不同。manifest 就是统一契约,运行时负责适配。
4.2 编写工具函数,遵守返回协议
video_meta.py核心代码如下:
#!/usr/bin/env python3 import json import os import subprocess import sys def video_metadata(file_path): if not os.path.exists(file_path): return { "ok": False, "error": "file_not_found", "message": f"文件 {os.path.abspath(file_path)} 不存在,请检查路径后重试。", } cmd = [ "ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", file_path, ] try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=15) except subprocess.TimeoutExpired: return { "ok": False, "error": "ffprobe_timeout", "message": "ffprobe 执行超时,文件可能过大或 ffprobe 未安装。", } if result.returncode != 0: return { "ok": False, "error": "ffprobe_error", "message": result.stderr.strip(), } probe = json.loads(result.stdout) streams = probe.get("streams", []) video_stream = None audio_stream = None for stream in streams: if stream.get("codec_type") == "video": video_stream = stream elif stream.get("codec_type") == "audio": audio_stream = stream data = { "container": probe.get("format", {}).get("format_name"), "duration_seconds": float(probe.get("format", {}).get("duration", 0) or 0), "bite_size": int(probe.get("format", {}).get("size", 0) or 0), "video": None, "audio": None, } if video_stream: data["video"] = { "codec_name": video_stream.get("codec_name"), "width": video_stream.get("width"), "height": video_stream.get("height"), "bit_rate": video_stream.get("bit_rate"), } if audio_stream: data["audio"] = { "codec_name": audio_stream.get("codec_name"), "sample_rate": audio_stream.get("sample_rate"), "channels": audio_stream.get("channels"), } return {"ok": True, "data": data} if __name__ == "__main__": # 从标准输入读取 JSON 参数 param = json.load(sys.stdin) result = video_metadata(param["file_path"]) print(json.dumps(result, ensure_ascii=False))这个工具脚本从标准输入接收 JSON 参数,再向标准输出打印 JSON 结果。Pi Agent 扩展协议里约定工具体采用这种 stdin/stdout 通信方式,隔离性好,也不容易产生字符串转义问题。
4.3 本地联调:观察 Agent 的“思考-调用”日志
写完扩展后,先手动模拟一遍调用:
echo '{"file_path": "test.mp4"}' | python3 tools/video_meta.py输出应该是:
{"ok": true, "data": {"container": "mov,mp4,m4a,3gp,3g2,mj2", "duration_seconds": 12.5, "video": {"codec_name": "h264", "width": 1920, "height": 1080}}}确认无误后,在 Pi Agent 的配置里将该扩展加入白名单,然后开启一条新对话,直接说一句“test.mp4 用的什么编码”。观察 Agent 的调用日志:它应当先检索 manifest 找到video_metadata,加载描述和参数 Schema,生成file_path参数,调用入口脚本,拿到 JSON 后总结回答。
在这个完整链路中,用户没有写任何工具用法提示词,Agent 也没有读到任何被“塞”进上下文的工具说明。这就是前面说的按需加载在实际运行中的样子。
4.4 发布与灰度:最小权限、最小噪音
扩展可以直接放到本地扩展目录,也可以打包成 zip 发布到团队仓库。发布前做三件事:第一,检查 permissions 有没有多余权限;第二,确认 description 里没有“不要”“除非”这类容易让模型困惑的双重否定表述;第三,准备一条冒烟用例,确保命令在干净的 Python 环境里也能运行。
我习惯把工具输出日志打开观察两三天,重点看“调用了哪几个工具、参数是否合理、返回数据模型是否能正常解析”。如果发现模型反复产生无效调用,多半是 Schema 描述不够明确,及时微调后再发布正式版本。
5. 常见问题排查与经验速查
扩展开发得越多,越会发现大部分问题都不是代码 bug,而是人和 Agent 之间的“通信协议”出了偏差。下面整理几个高频问题。
5.1 现象:工具已启用但 Agent 从不调用
先别怀疑模型笨,按照这个顺序排查:
| 排查点 | 说明 |
|---|---|
| 权限未授予 | 在配置文件里把工具加入allowed列表,否则 Agent 没有调用权限 |
| description 不够具体 | 如果 description 是一句“可以查看文件信息”这种话,模型很难把“H.265”和它关联起来 |
| 触发场景被其他工具抢占 | 多个工具描述相似时,模型会优先选择最“像”的那一个 |
| manifest 加载失败 | 用/tools list检查扩展是否真的被识别,安装路径是否正确 |
我遇到最多的是第二个原因。把 description 改成“读取本地视频文件中视频流、音频流与封装格式的元数据”之后,模型准确率明显提升。
5.2 现象:参数校验报错,模型总是画蛇添足
模型在生成工具参数时会参考对话历史。如果历史里出现过file_path带了引号包裹的写法,它就会学坏。解决办法是收紧 Schema 并增加additionalProperties: false,同时在 description 里明确“路径不要加引号,不要进行 JSON 转义”。
如果某个参数有固定选项,比如按编码类型过滤,就把它定义成 enum:
codec: type: string enum: - h264 - hevc - av1模型在 enum 约束下通常不会生成超出范围的取值。
5.3 现象:工具执行成功但 Agent 不会总结
工具返回了 JSON,但模型说得颠三倒四,或者只念数据不解读。这通常是返回值里缺少语义字段。比如查询视频信息时,如果设计里都知道用户想看“是不是 HEVC”,工具端直接返回:
{ "ok": true, "data": { "is_hevc": true, "codec_name": "hevc" } }模型就不需要自己推断,直接照着说就行。这个原则叫“让工具完成最后一公里解读”,能显著减少模型的自由发挥空间。
5.4 我的几点实用心得
- 工具名称用
snake_case,一眼能看出用途。video_metadata比vm好一百倍。 - 不要在 description 里写“在用户询问视频时可以使用本工具”这种叙述,模型对懒描述不敏感,对动作描述敏感。
- 错误信息要写给模型看,不是只写给用户看。明确告诉模型“file_not_found”之后应该怎么做,比如“请检查路径是否存在,如果路径由用户提供,请向用户确认”。
- 一个工具只做一件事。把“读取元数据 + 转换格式 + 上传服务器”拆成三个独立工具,Agent 能更灵活地组合,你调试时也更轻松。
- 工具提示词的减少不是一蹴而就的,配置完扩展后要持续观察真实会话日志,把模型反复出错的描述改掉,一周左右基本能稳定下来。
我在实际项目中踩过不少坑,最深的体会是:扩展作者都应该把自己当成“给 AI 写产品说明书”的产品经理。你的用户不是开发者,而是一个会读文字、会猜意图、偶尔还会误会的语言模型。你写得越清楚、越结构化,它就越少需要用户额外用提示词去纠正。这也是 Pi Agent 这类平台的扩展体系真正有价值的地方——工具能力一旦被良好封装,普通用户根本不需要学任何“咒语”,一句话就能让 Agent 干活。