Pi Extension API实战:让AI智能体调用自有脚本与服务
2026/9/5 13:29:06 网站建设 项目流程

Pi拆到第七篇,终于要聊一个很多人问过我的话题:当内置工具不够用的时候,怎么能让Pi按团队自己的逻辑干活。前几篇我们把Pi的上下文机制、模型调度、工具调用都过了一遍,工具调用那篇留言区里讨论最集中的几个问题,全都是同一个指向:Pi能不能调用我自己写的脚本?能不能访问我们内网的服务?能不能按我们公司的模板生成报告?这些问题靠改prompt解决不了,靠内置工具也解决不了。Pi给出的解决方案,就是Extension API。这篇文章不聊概念,直接把运行链路、接口设计、完整示例、排障经验全部摊开来讲,看完你就能动手写第一支自己的扩展。

1. 拆到第七篇,为什么偏偏要单讲Extension API

1.1 前面六篇的路线回顾

熟悉这个系列的朋友知道,前面六篇我们按照一条主线在拆:从Pi的安装启动讲起,然后是配置管理、上下文构造、模型调度策略、工具调用机制、多会话记忆。这条线其实就是Pi工作的完整过程——用户输入进来,Pi把历史、系统指令、工具定义组装成上下文,交给模型推理,模型决定调用什么工具,Pi执行工具并把结果回填,最后生成回答。

走到工具调用那一步时,很多人的感受是:Pi确实能读写文件、执行命令、抓网页,可一旦遇到"我们自己团队的内部逻辑",内置工具就使不上劲了。你没有办法让内置工具去查你们公司的工单系统,也没有办法让它按你们市场部的模板生成周报。这个时候,Extension API就是那个补位的东西。

1.2 没有Extension API时,Pi的边界在哪里

先看一个典型场景。假设团队希望Pi每周一自动聚合仓库里的Git提交,生成一份按作者分组的周报。没有扩展机制的时候,你只能做两件事:

第一种,手动复制。把git log的输出复制出来,扔给Pi让它总结。问题是提交一多,上下文被大量原始文本撑爆,而且每次都要重复这套动作,效率极低。

第二种,写一个独立脚本。你写好build_weekly_report.py,然后让Pi执行python脚本。脚本确实能跑,但Pi看不到结构化结果,只能拿到标准输出。你想让它"基于结果再分析一下哪几个人提交量下降",它拿到的只是一堆文本,没有可靠的字段,后续推理全凭猜。

这两种做法共同的缺陷是:任务的闭环断了。Agent的价值在于"发现问题—调工具—看结果—再调工具—给结论"这条循环。一旦循环中间需要人手工接驳,Pi就退化成了聊天框加命令行。Extension API要解决的核心问题,就是把这条循环重新焊上。

1.3 Extension API到底扩展了什么

我自己的理解是,Extension API扩展的不只是"工具数量",而是三个层次:

能力边界。任何能被代码表达的逻辑,都能变成Pi的一个新能力。查数据库、调内部接口、解析私有协议、生成特定格式的文件,这些都不再依赖内置工具。

信息闭环。扩展执行的结果是结构化回传的,模型能读到明确的字段。比如周报扩展返回按作者分组的markdown,Pi能基于它继续提问:"这周谁提交最多?"它可以从结果里直接找。

流程编排。多个扩展可以配合使用,一个扩展的返回结果可以成为另一个扩展的输入条件,让Pi从"单步问答"变成"多步骤的业务处理"。

如果打个比方,内置工具是Pi出厂自带的瑞士军刀,而Extension API是让你能自己设计刀头、拧上去换用的那套系统。没有这套系统,军刀再锋利,也切不了你们公司特有形状的工件。

2. Extension的核心运行链路:一次调用是怎么走完的

2.1 三个核心部分:manifest、runtime、schema

一个Extension在Pi里由三部分组成,缺一不可。

manifest是扩展的身份证。它声明了扩展叫什么、版本号、入口是哪个文件、用什么runtime跑,以及最关键的部分——triggers(触发声明)。Pi在每次会话启动时,会读取所有已启用扩展的manifest,把triggers里的描述注入给模型。

schema是模型决定"什么时候调用、传什么参数"的依据。它本质上是一份JSON Schema,描述这个工具接受哪些参数、每个参数的含义、哪些是必填的。模型会阅读schema,判断用户意图是否匹配,然后构造出符合规范的调用参数。

runtime是实际执行环境。当前Pi的扩展runtime以node和python为主。要注意,Pi不是把你的脚本丢给系统shell直接执行,而是由调度器加载模块、调用导出函数。这层封装决定了你能用ctx拿到哪些能力,也决定了权限控制是怎么做进去的。

三者的关系可以这么记:manifest告诉Pi"有什么",schema告诉模型"怎么用",runtime负责"真正跑起来"。

2.2 从用户消息到扩展执行完毕的完整链路

一次扩展调用从触发到返回,完整的路径是下面这样。

第一步,用户输入。Pi把系统提示、对话历史、所有启用扩展的trigger定义一起组装成上下文,发给底层模型。这一步里,你的扩展对模型来说只是一个"可能被调用的工具",模型并不知道扩展背后的实现细节。

第二步,模型决策。模型读完上下文,判断用户意图是否匹配某个扩展的description和schema。如果匹配,它会在回复中输出一个结构化的工具调用,里面包含扩展名和JSON格式的参数。

第三步,调度。Pi收到工具调用后,先做校验:参数是否符合schema、扩展是否在权限配置内、是否超出并发限制。校验通过,调度器加载对应runtime,调用扩展入口里导出的execute函数,传入两个参数——ctx(运行时上下文)和args(模型给的参数)。

第四步,执行。扩展内部通过ctx提供的辅助函数完成实际动作。比如执行git log、读取文件、调内部接口。执行完毕后,扩展返回一个结构化的结果对象。

第五步,回填。Pi把结果对象交给模型,模型结合结果生成最终回答,或者决定发起下一次工具调用。整个过程对用户来说是无感的,他们看到的只是Pi"真的会搞周报"了。

这条链路里最值得留意的是第二步和第五步——模型两次介入。扩展能不能被正确触发,取决于你写的description和schema清不清楚;扩展的价值能不能被放大,取决于你返回的结果够不够结构化。这两点后面展开讲。

2.3 数据在模型、调度器、扩展之间怎么流转

对于写扩展的人来说,最关心的就是ctx和返回值。

ctx是Pi封装好的运行时上下文,不是裸Node环境。里面常用的有:

  • cwd:当前工作目录,通常是用户启动Pi时所在的目录
  • user:当前用户信息
  • env:经过过滤的环境变量,不是所有环境变量都会透传
  • logger:结构化日志接口,会记录到Pi的扩展日志里
  • runCommand:执行外部命令的安全封装,受权限策略约束
  • readFile / writeFile:受路径白名单约束的文件读写
  • writeTemp:写入临时目录

返回值一般有几种类型:

  • text:普通文本,模型会直接读
  • markdown:结构化文本,适合周报、表格这类内容
  • json:机器可读的数据,模型会作为结构化观察来消费
  • error:显式错误,Pi会把错误信息回填给模型,让模型决定下一步
  • async:异步任务,返回taskId,后续通过轮询机制拿结果

当初我写第一个扩展时犯过一个典型错误:把git log解析成一段漂亮的文本返回给用户,但模型无法基于它做进一步分析。后来改成返回markdown分组结果,模型可以直接从里面提取作者名字、提交数量、日期区间,追问"上周谁提交最少"这类问题就完全不需要重新执行命令。设计返回值时,脑子里要时刻想着"模型拿到这个结果能不能接着算"。这是Extension开发里最容易被忽略、也最关键的一点。

3. 手写一个真实可用的Extension:从注册到上线的全流程

3.1 第一支扩展选什么

我不会劝你第一个扩展就做"调用公司内部CRM系统"这种高难度动作。第一支扩展应该满足三个条件:逻辑简单、依赖现成工具、结果直观可见。最合适的就是git周报生成器。

理由很实在:几乎所有开发者都有Git环境,不涉及额外认证;git log命令本身就是结构化输出,解析难度低;周报结果一眼就能看出扩展有没有正常工作,方便验证。等你把这个跑通了,再去接内部API、做复杂编排,就有了一个清晰的上手路径。

3.2 定义manifest与trigger schema

先建一个目录,假设叫git-weekly,在目录里创建manifest.json:

{ "name": "git-weekly", "version": "1.0.0", "description": "汇总Git仓库最近一段时间的提交记录,生成结构化周报", "runtime": "node", "entry": "index.js", "triggers": [ { "type": "tool", "name": "generate_weekly_report", "description": "当用户需要根据Git提交记录生成周报、日报、提交汇总或迭代总结时使用。适合团队同步、周会材料整理、个人工作总结等场景。", "schema": { "type": "object", "properties": { "since": { "type": "string", "description": "起始日期,格式YYYY-MM-DD,例如2025-05-01" }, "until": { "type": "string", "description": "结束日期,格式YYYY-MM-DD,默认是今天" }, "repo_path": { "type": "string", "description": "Git仓库路径,默认为当前工作目录" }, "group_by": { "type": "string", "enum": ["author", "date", "none"], "description": "周报分组维度,默认按作者分组" } }, "required": ["since"] } } ] }

注意triggers数组里的name,就是我们前面链路里说的"模型会输出的工具调用名"。description要尽量包含触发场景的关键词。我见过太多扩展不触发,问题就出在description写得太笼统,比如只写一句"生成周报",模型遇到"帮我看看这周大家干了啥"这种口语化表达时,根本联想不到要调它。schema里每个参数都要写清楚格式和语义,比如日期字段要标注"YYYY-MM-DD",否则模型容易传一个"上周一"进去,你的解析逻辑还得兼容人类语言,复杂度立刻上升。

3.3 实现核心逻辑

在同一个目录下创建入口文件index.js:

import { exec } from 'node:child_process'; import { promisify } from 'node:util'; const run = promisify(exec); export default { async execute(ctx, args) { const since = args.since; const until = args.until || new Date().toISOString().slice(0, 10); const repo = args.repo_path || ctx.cwd; const groupBy = args.group_by || 'author'; ctx.logger.info('git-weekly start', { since, until, repo, groupBy }); const commitFormat = groupBy === 'author' ? '%an\t%s\t%ad' : '%ad\t%s\t%an'; const command = `git -C "${repo}" log --since="${since} 00:00:00" --until="${until} 23:59:59" --pretty=format:${commitFormat} --date=format:%Y-%m-%d`; const { stdout } = await run(command).catch((err) => { ctx.logger.warn('git log failed', { message: err.message }); return { stdout: '', stderr: err.message }; }); const lines = stdout.split('\n').filter(Boolean); const commits = lines.map((line) => { const parts = line.split('\t'); return groupBy === 'author' ? { author: parts[0], message: parts[1], date: parts[2] } : { date: parts[0], message: parts[1], author: parts[2] }; }); if (commits.length === 0) { return { type: 'text', content: `在 ${since} 到 ${until} 之间没有发现提交记录,请确认时间范围和仓库路径是否正确。` }; } const grouped = {}; for (const item of commits) { const key = groupBy === 'none' ? 'commits' : (groupBy === 'author' ? item.author : item.date); grouped[key] = grouped[key] || []; grouped[key].push(`- ${item.message}(${item.date})`); } let md = `## Git周报 ${since} ~ ${until}\n\n`; for (const [key, items] of Object.entries(grouped)) { md += `### ${key}\n\n`; md += items.join('\n') + '\n\n'; } return { type: 'markdown', content: md }; } };

这段代码有几个地方值得展开说。repo参数的默认值用了ctx.cwd,这是很自然的回退逻辑,但前面提醒过,cwd不一定是仓库根目录,所以这个字段应该允许用户显式传入。命令执行用catch兜底,原因是不让异常直接抛给Pi调度器——扩展一旦抛异常,Pi默认只会把"工具调用失败"回填给模型,具体错误信息很容易丢。把错误记到ctx.logger里,再把提示性内容返回,排查时才能看到完整上下文。返回markdown类型而不是text,是为了让模型对结果的分组结构有清晰认知。

如果你更熟悉Python,也可以用python runtime。差别只在入口文件的写法,manifest里把runtime改成"python",entry指向xxx.py,Python代码约定导出execute函数即可,参数结构完全一致。

3.4 注册与验证

写完之后,把整个git-weekly目录放到Pi的扩展目录下。不同版本路径有差异,一般是~/.pi/extensions/。放好后执行pi ext reload,让Pi重新扫描扩展目录。用pi ext list确认扩展状态为enabled,这一步可以看到扩展有没有加载成功,也可以检查manifest里有没有语法错误。

验证环节,我建议按这份清单走:

  • 在对话里输入"帮我生成本周周报",不指定仓库路径,看它是否自动用当前目录
  • 输入"看看上周大家提交了什么",验证触发词换一种说法扩展还能不能命中
  • 带参数提问,比如"按日期分组,生成上周一到上周五的日报"
  • 故意问一个不相关的问题,比如"什么是装饰器模式",确认扩展不会误触发
  • 查看日志确认每次调用的入参和返回结果,核对数据是否正确

如果验证过程中发现模型不触发、触发错乱、参数不对,大概率不是代码问题,而是manifest和schema的问题。这个判断很关键,能帮你快速缩小排查范围。

4. 我踩过的坑:Extension开发中的典型故障与排查链路

4.1 症状1:模型一直不触发扩展

这是一个极其常见的开局。扩展装上了,list里是enabled,但你问"这周代码提交情况怎么样",Pi要么直接说"我看看",然后跑内置命令,要么干脆给你一段泛泛的回答,就是不调用你的扩展。

这时候先别怀疑代码,问题几乎都出在trigger定义。排查链路我在实际项目里走了很多次,总结成三步。

第一步,进入debug模式,查看模型真正收到的工具定义。Pi有扩展调试命令,比如pi ext debug git-weekly,开启后Pi会打印注入给模型的所有工具描述。看到这份"模型视角"的描述,很多问题就一目了然。

第二步,检查description。如果description里只有"生成周报"三个字,模型大概率不知道什么时候用。我自己的经验是,把触发场景、使用时机、典型句式都写进去。比如"当用户需要根据Git提交记录生成周报、日报、提交汇总或迭代总结时使用,适合团队同步、周会材料整理、个人工作总结等场景"。用户说"帮我整一下这周的活",你希望模型能联想到这个扩展,那description里最好就出现"总结""这周""周会"这类高频表达。

第三步,检查schema的复杂度。如果必填字段过多,模型可能因为"拿不准参数"而选择不调用。比如你要求repo_path必填,但用户没说仓库路径,模型不知道填什么,干脆放弃。解决方案是尽量让字段可选、提供默认值,把"推断路径"的活交给扩展内部逻辑,而不是压给模型。

4.2 症状2:扩展执行超时或卡死

扩展触发了,但Pi一直转圈,最后提示工具调用超时。这种情况在同步调用的扩展里非常常见。

原因有两类。一类是扩展内部有长任务,比如批量克隆、复杂聚合计算,执行时间超过了Pi对同步扩展的默认上限。另一类是外部命令挂起,比如git命令等待输入密码、网络请求迟迟不响应。

排查时先看日志,确认卡在哪一步。如果是命令挂起,优先检查你是否用了需要交互的命令,以及是否设置了env超时机制,例如给git配置GIT_TERMINAL_PROMPT=0。如果是长任务,方案是改成异步模式:Pi支持返回{ type: 'async', taskId },让Pi通过轮询机制后续获取结果。扩展里要实现一个poll方法,Pi会在任务执行期间周期性调用它,直到返回终态。

另一个容易忽略的点是超时配置。每个扩展可以在manifest或权限配置里单独声明timeout,不是所有场景都适合默认值。但我建议短任务能同步就同步,异步会引入复杂度,只有确实有必要才用。

4.3 症状3:拿不到预期上下文

有段时间我的扩展里写了相对路径,在对话里一切正常,一换到子目录启动Pi就报文件找不到。排查后发现ctx.cwd不是我以为的那个目录。用户在哪个目录启动Pi,cwd就是哪里,它不等于仓库根目录,也不等于扩展目录。

这类问题的排查思路是在execute开头先做一次上下文dump,把cwd、关键env、参数值都打出来:

ctx.logger.info('git-weekly env', { cwd: ctx.cwd, args, home: ctx.env.HOME, hasGit: !!(await run('git --version')).stdout });

看到实际值之后,很多"灵异问题"就变成具体问题了。经验是:扩展内部永远不要假设路径,所有路径要么来自参数,要么基于cwd做显式拼接后单独校验。环境变量同理,Pi默认不会把用户所有环境变量透传给扩展,只有白名单内的变量能拿到。如果扩展依赖某个环境变量,要么在权限配置里显式声明,要么让用户通过参数传入。

4.4 诊断方法论:日志、回放与最小复现

调试扩展最怕的就是"在对话里反复试"。一次失败,你改一行代码,再开一个对话,往往光等模型输出就要花好几分钟。我的习惯是三步走。

第一步,看日志。Pi的扩展日志是结构化的,开启debug后能看到每次调用的入参、返回值、异常堆栈。这比看模型聊天输出可靠得多。

第二步,用回放。Pi会记录扩展调用记录,通过pi ext replay可以查看某次调用的完整输入和输出。模型给的参数到底是什么、扩展返回了什么、模型后续怎么消费的,一目了然。这个工具对复现问题极其有用。

第三步,做最小复现。把扩展里的核心逻辑抽出来,写一个独立脚本,手动构造args调用execute函数,绕过对话层直接跑。比如git-weekly,我就写过一个test.js,传死参数,直接调用execute,确认逻辑没问题再回到Pi里做集成验证。这一步能帮你把"扩展自己的bug"和"Pi调度层的bug"分开,排查效率翻倍。

5. 把扩展做成体系:组合、权限与资源治理

5.1 多扩展协作与条件路由

扩展数量一多,新的问题出现了:当用户说"扫描一下这个项目的安全情况",Pi面前站着三个扩展——命令行扫描器、依赖漏洞检查器、密钥泄露检测器,它该调哪个?

Pi的调度策略本质上还是依赖trigger描述。想让多个扩展各司其职,就得在description里划清楚边界。密钥泄露检测的description要明确写"适用于扫描硬编码的API密钥、密码、token等敏感信息",而依赖漏洞检查要写"适用于检查package.json、requirements.txt等依赖清单中的已知安全漏洞"。描述互相不重叠,模型才能精准路由。

如果需要多个扩展协作,比如先扫描生成清单、再根据清单生成报告,扩展之间不能直接互相调用,正确的做法是写一个编排扩展。它内部把这几个动作串起来,对外只暴露一个更高层的工具。这样模型面对的还是一个清晰工具,复杂度被封装在扩展内部了。

5.2 权限隔离:扩展能碰什么、不能碰什么

扩展本质上是不可信代码,尤其当团队里多人共享扩展时,权限隔离是从第一天就要考虑的事。Pi对扩展的权限模型是"最小权限",不是"给一个shell看运气"。

实操中有几个准则。命令执行要用允许前缀列表,而不是让扩展随便拼shell命令。比如git-weekly,最理想的权限是只允许git log和git shortlog,不允许git push、git clean这类危险操作。文件读写要限制在指定目录范围,尤其是共享机器上,扩展不应该默认能读整个用户目录。环境变量要按需注入,默认只给PATH、HOME这类基础变量。网络请求默认拦截,除非显式允许,否则扩展不能私自往外传数据。

权限配置大致长这样:

{ "git-weekly": { "timeout": 30, "cwd": "$WORKSPACE", "env": ["PATH", "HOME", "GIT_TERMINAL_PROMPT"], "allow": [ "git log", "git shortlog" ], "deny": [ "git push", "git clean" ] } }

原因很简单:你永远不会知道别人提交的扩展代码里藏着什么。哪怕开发者本人没有恶意,一个不慎也足以造成破坏。权限宁可严,不可宽,这是项目协作里的安全底线。

5.3 资源限制与降级策略

权限之外,资源限制是第二个治理重点。常见的有四类:timeout控制单次执行时长,maxOutputSize防止扩展返回超大数据撑爆上下文,maxConcurrency限制同时执行的扩展数量,maxRetries限制失败重试次数。这些参数都可以在权限配置里按扩展单独设置。

另一个值得提到的是自动停用。当扩展连续失败多次,Pi会自动停用它,避免一个坏扩展拖垮整个会话。实际效果是:对话里Pi会提示"git-weekly当前不可用,我改用内置命令查看提交记录",而不是无限报错。

降级策略我在团队里明确要求过:任何扩展被设计成"锦上添花"而不是"唯一通道"。比如周报扩展挂了,Pi回退到执行git log再人工总结也能完成任务,只是效果差一些。把扩展做成可选增强,而不是单点依赖,系统整体健壮性会好很多。

6. 与MCP、内置工具、SDK深调的取舍

6.1 三种扩展方式的适用场景对照

写了这么多扩展,很多人会问:那和MCP有什么区别?Pi不是也支持MCP吗?这个问题我几乎每篇都会被问,直接上一个对比表。

对比项内置工具Extension APIMCP接入SDK深度集成
面向用户所有用户会写脚本的开发者团队或工具开发者把Pi嵌入自己产品的团队
能力来源Pi内置本地私有逻辑第三方服务与数据源完全自定义
接入成本低,一个目录加脚本中,需要独立进程的MCP Server高,需要重新构建
执行位置Pi进程内Pi扩展沙箱独立进程你的应用进程
适合场景通用日常操作团队内部业务逻辑外部生态接入产品化集成

从这张表能看出一条清楚的决策路径:通用能力用内置工具,私有逻辑用Extension API,外部服务用MCP,产品化用SDK。它们不是替代关系,而是从低到高的四个层次。

6.2 什么时候不该用Extension API

Extension API不是银弹,我认为有三类场景应该主动避开。

第一类是一次性任务。如果你只是想让Pi临时跑一个脚本,直接把脚本内容发给Pi让它执行,比打包成扩展划算得多。扩展的优势在复用,一次性任务不值得维护成本。

第二类是成熟的外部服务接入。如果某个服务已经有MCP Server,优先接MCP。因为MCP的能力和schema通常是服务方维护的,你不用自己跟进接口变化。

第三类是深度产品集成。如果你想把Pi嵌入自己的IDE插件或业务系统,应该用SDK,在应用代码里控制Pi的生命周期和工具注册。Extension API面向的是"在Pi运行时里加能力",不是"在自己产品里嵌Pi"。

扩展开发最大的隐性成本是维护。schema要跟着模型行为调、runtime要跟着Pi版本升、权限配置要跟着安全要求改。如果这个能力只是给一个人用的临时脚本,真的不值得。

6.3 我的选型经验

分享一些实际的选型判断标准。决策顺序我通常是:内置工具优先,覆盖不了再看Extension API,需要外部生态再看MCP,SDK最后才考虑。因为越往顶层,开发和维护成本越高,能用下层解决的就不升上去。

扩展的拆分粒度也很重要。我的原则是按业务域拆,不按函数拆。一个"代码审查扩展"可以内部包含静态检查、依赖扫描、提交人分析多个动作,但对外只暴露一个"code_review"工具。拆得太细,模型面对一堆功能重叠的工具,路由准确率反而下降。

版本管理方面,manifest里维护好version字段,整个扩展目录用Git管理,线上扩展更新走CI加schema校验。这套流程看起来重,但一旦扩展数量过了五个,没有版本管理的代价远高于搭这套流程的成本。

回看Pi从助手变成平台的过程,Extension API确实是分水岭。在它出现之前,Pi的能力边界由官方决定,你只能等在发布清单里看到新工具的那一天。有了Extension API之后,边界由每一个使用者自己定义。如果你正准备动手写第一支扩展,我的建议是先别急着写代码,把manifest和schema写好,再打开debug模式,仔细看看模型眼里你的扩展长什么样。这一步想明白了,剩下的都是体力活。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询