我以为Agent技能就是"加了参数的API",结果第一个坑在第三行就踩了

"不就是多传几个参数吗?"

这是我第一次看到Perplexity Agent Skills文档时的第一反应。然后我花了半天时间,在第一个技能模块上反复失败——代码没问题,注册也成功了,就是不触发。

后来我才明白:我用的是API的思维在做Agent开发,这两件事根本不是一个物种。

这篇文章是我啃完Perplexity官方Agent Skills开发者手册(Developer Guide v2.1,目前通过Perplexity开发者门户公开获取)之后的完整实录。不是翻译,不是摘要,是真实的"踩坑→理解→跑通"全过程。

只需要会基础Python,不需要懂LLM原理,跟着走就能跑通第一个技能模块。

---

第一章:为什么去啃这份手册?它和普通API文档的根本差异

普通API文档告诉你怎么调;这份手册告诉你Agent怎么想

这句话听起来像废话,但它是真实的认知差距。

绝大多数开发者接触AI工具的路径是:注册→拿到API Key→看文档→传参数→拿结果。整个过程你是主动的,你决定什么时候调、传什么、期望什么输出。这是命令式的交互模型。

Agent Skills完全不一样。你写的不是"调用逻辑",而是"能力描述"——你告诉系统"我能做什么",然后等LLM决定要不要用你。你从司机变成了乘客。

这个认知跳跃,比背100个参数字段更重要。

Perplexity的这份手册之所以值得专门去读,是因为它是少数把这层逻辑显式写出来的文档。大多数Agent框架的文档只告诉你怎么注册技能,不告诉你LLM为什么会或不会路由到你的技能——而后者才是实际开发中最难调试的部分。

---

第二章:从零搭一个最简单的技能模块——完整流程实录

进度条:这一章是基础,跑通之后你才有资格理解第三章的核心差异。

环境准备

依赖极简,Python 3.9+,装两个包:

pip install perplexity-agent-sdk requests

实操这个教程需要调用兼容的API接口。我这次测试用的是 [api.884819.xyz](http://api.884819.xyz),延迟稳定,省去了大量网络折腾的时间。注册即送体验token,国产模型完全免费,按量付费,没有月租。

把你的endpoint和key配置好:

import os

os.environ["AGENT_API_BASE"] = "https://api.884819.xyz"

os.environ["AGENT_API_KEY"] = "your_key_here"

技能定义

这是最小可跑通的技能模块,功能是"查询某城市当前天气":

from perplexity_agent_sdk import Skill, SkillParam, skill_registry

@skill_registry.register

class WeatherSkill(Skill):

name = "get_weather"

description = (

"当用户询问某个城市的天气、气温、是否下雨、"

"穿什么衣服时,调用此技能获取实时天气信息。"

)

params = [

SkillParam(

name="city",

type="string",

description="城市名称,如'北京'、'上海'",

required=True

)

]

def execute(self, city: str) -> dict:

# 这里替换成真实天气API

return {

"city": city,

"temperature": "22°C",

"condition": "晴",

"suggestion": "适合外出,无需带伞"

}

⚠️ 踩坑点1description字段用中文没问题,但要具体描述触发场景,不能只写功能名称。我最初写的是"获取天气信息",结果技能一次都没被路由到——后面第三章会详细说为什么。

注册与启动

from perplexity_agent_sdk import AgentServer

server = AgentServer(

api_base=os.environ["AGENT_API_BASE"],

api_key=os.environ["AGENT_API_KEY"]

)

server.load_skills() # 自动扫描已注册的技能

server.start(port=8080)

触发测试

注册成功后,终端会输出类似:

[AgentServer] Skills loaded: 1

✓ get_weather — "当用户询问某个城市的天气..."

[AgentServer] Listening on http://localhost:8080

发送测试请求:

curl -X POST http://localhost:8080/chat \

-H "Content-Type: application/json" \

-d '{"message": "北京今天天气怎么样,要带伞吗?"}'

首次触发成功的响应:

{

"skill_invoked": "get_weather",

"params": {"city": "北京"},

"result": {

"city": "北京",

"temperature": "22°C",

"condition": "晴",

"suggestion": "适合外出,无需带伞"

},

"response": "北京今天晴天,气温22°C,不需要带伞,适合外出。"

}

看到"skill_invoked": "get_weather"的那一刻,是整个流程里最有成就感的瞬间。

⚠️ 踩坑点2:调试阶段一定要看skill_invoked字段,而不只看最终response。如果技能没被路由,response依然会有内容(LLM会自己编一个答案),你很可能以为技能触发了,其实根本没有。

---

第三章:最不一样的那个环节——意图路由 vs. 参数匹配

进度条:现在到了最关键的地方。这一章是整篇文章的核心。

两种调用模型的本质差异

| 维度 | 普通API调用 | Agent Skills调用 | | 触发方式 | 开发者主动调用 | LLM根据意图决定是否调用 | | 参数来源 | 开发者显式传入 | LLM从用户输入中提取 | | 调用时机 | 确定性,代码控制 | 概率性,语义控制 | | 失败模式 | 报错/异常 | 静默跳过(最难发现) | | 调试方式 | 看错误日志 | 看路由决策日志 |

这张表里最重要的是第四行:失败模式

普通API调用失败,你会看到报错。Agent Skills路由失败,什么都不会发生——LLM会用自己的知识直接回答,你的技能被完全绕过,而且没有任何提示。

这就是为什么我的天气技能"注册成功但永远不触发"——它不是崩了,它只是从未被选中过。

意图路由决策链

用户输入:"北京今天要带伞吗?"

┌─────────────────────────────┐

│ LLM读取所有已注册技能的 │

│ name + description字段 │

└─────────────┬───────────────┘

┌─────────────────────────────┐

│ 语义匹配:用户意图 │

│ 是否与某技能描述相符? │

└──────┬──────────────┬───────┘

│ 匹配 │ 不匹配

▼ ▼

┌──────────┐ ┌──────────────┐

│ 调用技能 │ │ LLM直接回答 │

│ 提取参数 │ │ 技能被跳过 │

└──────────┘ └──────────────┘

关键认知:LLM做路由决策时,它看的是你的description字段,不是你的代码逻辑。你的execute方法写得多精妙都没用,如果description没有覆盖用户可能的表达方式,技能就是隐形的。

这就是为什么"获取天气信息"这个描述会失败——它描述的是功能,而LLM路由时匹配的是用户意图的语言模式。用户不会说"我要获取天气信息",他们会说"要带伞吗""穿什么好""今天热不热"。

---

第四章:三个最容易误解的地方(避坑清单)

① Description字段比代码本身更重要

错误写法:
description = "获取城市天气数据"
正确写法:
description = (

"当用户询问某个城市的天气、气温、是否下雨、"

"穿什么衣服、要不要带伞、今天冷不冷时,"

"调用此技能获取实时天气信息。"

)

💡 写description的正确姿势:想象用户会怎么问这个问题,把那些口语化表达全部列进去。Description是你技能的"招聘广告",LLM是HR,它根据JD决定要不要用你。

② 错误处理不能用传统try-catch思维

传统API开发中,你会这样处理异常:

# ❌ 在Agent Skills里这样做是危险的

def execute(self, city: str) -> dict:

try:

result = call_weather_api(city)

return result

except Exception as e:

raise e # 抛出异常,让调用方处理

问题在于:Agent Skills的调用方是LLM,它不知道怎么处理Python异常。技能抛出异常后,行为是未定义的——有时LLM会重试,有时直接跳过,有时给用户一个莫名其妙的错误消息。

正确做法:永远返回结构化的错误信息,让LLM自己决定怎么告知用户:
# ✅ 正确:把错误信息结构化返回

def execute(self, city: str) -> dict:

try:

result = call_weather_api(city)

return {"success": True, "data": result}

except Exception as e:

return {

"success": False,

"error": f"无法获取{city}的天气信息,请稍后重试"

}

③ 多技能并发时的优先级冲突

当你注册了多个技能,LLM有时会面对"这个问题好像两个技能都能答"的情况。

比如你同时有get_weathertravel_advice两个技能,用户问"北京适合旅游吗"——两个技能的description都可能匹配到这个问题。

错误做法:让两个技能的description存在语义重叠区域,然后祈祷LLM选对。 正确做法:在description里显式划定边界:
# travel_advice的description

description = (

"当用户询问某地的旅游景点、行程规划、文化特色、"

"消费水平时,调用此技能。"

"注意:天气相关问题请使用get_weather技能。"

)

是的,你可以在description里直接提到其他技能的名字,告诉LLM边界在哪里。这个技巧在手册里只有一句话,但它救了我很多调试时间。

---

第五章:跑通之后,下一步能做什么?

最简单的技能模块跑通之后,你面前有三条路可以走:

方向一:接入外部数据源

execute方法里的mock数据换成真实API调用——天气、搜索、数据库查询都行。核心要注意的是响应时间:Agent Skills默认有超时限制,外部API调用超时后技能会被标记为失败。建议在execute里加本地缓存层。

方向二:链式技能调用

一个技能的输出可以成为另一个技能的输入。比如get_weather返回天气数据后,outfit_advisor技能可以消费这个数据给出穿衣建议。SDK提供了skill_chain装饰器来声明这种依赖关系。

方向三:自定义路由策略

如果你对LLM的默认路由决策不满意,可以实现自定义的SkillRouter类,在LLM决策之前加一层规则引擎——比如"用户问题包含特定关键词时强制路由到某技能"。这是高级用法,但在生产环境里往往是必须的。

---

一句话总结

技能模块的核心是描述,不是代码。

你的execute方法是技能的身体,description是技能的灵魂。身体再强壮,没有灵魂也不会被召唤。

立即能做的一个动作:把你现有的任何一个工具函数,用上面的模板包装成一个Skill,重点花时间打磨description——用10种不同的方式描述用户可能的提问方式。跑一遍,看看它是否能被正确路由。

如果你想直接复制我的代码跑起来,记得把endpoint换成你自己的地址,我用的是 [api.884819.xyz](http://api.884819.xyz),注册即送体验token,开箱即用,国产模型完全免费。

---

最简单的技能模块跑通之后,我发现真正有意思的问题才刚开始:

当你有5个技能同时注册,LLM怎么决定调哪个?优先级冲突的时候会发生什么?如果两个技能的description高度相似,LLM的选择是稳定的还是随机的?

这个问题我已经测了两天,做了超过200次触发实验,结果出乎意料——LLM的路由选择远比你想象的更"脆弱",一个词的差距可以让命中率从90%掉到30%。

下篇文章写多技能路由冲突的完整拆解,如果你也在做Agent开发,那篇可能比这篇更有用。

---

本文由8848AI原创,转载请注明出处。关注8848AI,带你从零开始学AI。

#AI教程 #Agent开发 #Perplexity #LLM #Python #8848AI #AI工具 #大模型开发