调用 API 是在"问",写 Skills 是在"授权"
本文最后更新于 2026-05-10,文章内容可能已经过时。
调用 API 是在"问",写 Skills 是在"授权"——Perplexity 手册里藏着3个反直觉的 Agent 开发铁律
大多数开发者读官方文档的方式是这样的:跳过前言,跳过设计哲学,直接找"Quick Start",复制代码,跑起来,完事。
Perplexity 的 Agent Skills 开发手册,大概有 90% 的人就是这么读的。
但手册里有一句话,被大多数人跳过了:
"Skills require a new developer mindset."
这句话不是废话,不是客套的产品文案。手册接下来用整整三章来解释"哪里不一样"——三章,每章都有具体的规范、反例和工程要求。
跳过这三章的后果是:你写出来的 Skill 能跑,但 Agent 不调它;或者调了,但遇到错误就陷入死循环;或者在多步骤任务里莫名其妙产生幻觉式错误,你查半天 bug 查不出来。
本文替你把这三章读完,拆成3个最具体的设计决策。读完这篇,等于提前踩过了别人要花3个月才能踩到的坑。
---
第一章:执行时机不由你控制——你以为在控制它,其实在说服它
这是最反直觉的一点,也是 Skills 和传统 API 调用最根本的差异。
传统 API 调用的逻辑是确定性的:你写代码,你决定什么时候调用、调用几次、传什么参数。整个控制流在你手里。# 传统写法:你掌控一切
result = search_api.query(keyword="AI news", limit=10)
if result.status == 200:
process(result.data)
Skills 的逻辑完全不同:你写的是"能力描述",Agent 的 Planner 层来决定是否调用这个 Skill、何时调用、用什么参数——你交出了控制权。
你不是在写一个函数,你是在向一个 LLM 推销一种能力。
手册里对 description 字段的定义非常明确:这个字段不是给人看的注释,它是 Planner 层用来做"技能选择"决策的核心输入。手册原文的表述大意是:Planner 完全依赖 description 来判断这个 Skill 是否适用于当前任务,函数体本身对 Planner 是不透明的。
这意味着什么?
一个写得烂的 description,会让你精心实现的 Skill 被彻底忽略。
来看两个对比:
# ❌ 开发者习惯的写法(给人看的)
description = "Fetches data from the news API"
✅ Skills 要求的写法(给 Planner 看的)
description = (
"Retrieves the latest news articles on a given topic. "
"Use this skill when the user asks about recent events, "
"breaking news, or wants to know what happened in the past 24-48 hours. "
"Do NOT use this for historical facts or general knowledge questions."
)
注意差异:Skills 的 description 必须包含触发条件(什么情况下用)和排除条件(什么情况下不用)。
如果你只写"Fetches data from the news API",Planner 面对"最近 AI 圈发生了什么"这个问题,很可能不知道该不该调你的 Skill——因为你没告诉它。
这一章的金句:你不是在写函数,你是在写一份"招聘说明书",告诉 Agent 什么时候该雇用这个能力。
---
第二章:错误处理必须对 Agent 友好,而不是对人友好
工程师写错误处理,本能是给人看的:抛 Exception,返回 HTTP 4xx/5xx,打印一行 "Connection timeout, please retry"。
这套逻辑在 Skills 里会造成严重问题。
原因很简单:当你的 Skill 出错时,接收错误信息的不是人,是 LLM。LLM 要根据你的错误信息做决策——是重试?换参数?还是放弃这个 Skill 转而用别的方式?如果你的错误信息是机器码风格的,LLM 会陷入困境。
手册里给出了一个典型的反例:
# ❌ 传统错误处理(对人友好,对 Agent 有毒)
def search_news(query: str):
try:
response = requests.get(API_URL, params={"q": query}, timeout=5)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
raise Exception("timeout")
except requests.exceptions.HTTPError as e:
raise Exception(f"HTTP {e.response.status_code}")
当 Agent 收到 {"error": "timeout"} 时,它不知道该怎么办——应该立刻重试吗?等多久?换个 Skill 吗?这个错误是暂时的还是永久的?
信息不足,Agent 只能猜,猜错了就死循环,或者在最终回复里编造一个答案(这就是"幻觉式错误"的来源之一)。
手册要求的格式是这样的:
# ✅ Skills 错误处理(对 Agent 友好)
def search_news(query: str):
try:
response = requests.get(API_URL, params={"q": query}, timeout=5)
response.raise_for_status()
return {"status": "success", "data": response.json()}
except requests.exceptions.Timeout:
return {
"status": "error",
"error_type": "transient",
"message": "The news API timed out. You may retry this skill once with the same parameters. If it fails again, inform the user that real-time news is temporarily unavailable.",
"retryable": True
}
except requests.exceptions.HTTPError as e:
if e.response.status_code == 429:
return {
"status": "error",
"error_type": "rate_limit",
"message": "Rate limit reached. Do not retry. Use cached results if available, or ask the user to wait 60 seconds.",
"retryable": False
}
关键差异在于:
error_type:告诉 Agent 这是暂时性错误还是永久性错误message:直接用自然语言告诉 Agent 下一步该怎么做retryable:明确的布尔值,不让 Agent 自己猜
这不是多此一举,这是在给 Agent 提供"决策树",而不是一个让它自己解读的谜语。
这一章的金句:你的错误信息,是 Agent 在出错时唯一的导航系统。写得含糊,它就会在黑暗里乱走。
---
第三章:状态管理是 Skill 的禁区——最隐蔽的坑
这是三个差异里最容易被忽略的,也是在多步骤 Agent 任务里最致命的。
传统开发者的习惯:在服务里维护 session、cache、上下文状态,这是很正常的工程实践。比如一个搜索服务,你可能会缓存用户的上一次查询,方便下次"继续搜索"。# ❌ 有状态的 Skill(危险)
class NewsSearchSkill:
def __init__(self):
self.last_query = None
self.session_cache = {}
def search(self, query: str = None):
if query is None:
query = self.last_query # 依赖上一次的状态
self.last_query = query
result = fetch_news(query)
self.session_cache[query] = result
return result
这段代码在单用户、单线程、顺序执行的场景里没问题。但放进 Agent 框架里,会产生一种非常诡异的错误。
原因是:Agent 框架可能并发调用同一个 Skill,也可能在不同的执行分支里调用它,还可能在重试时重新调用。你的 last_query 会被污染,session_cache 会在不同任务之间串扰,最终 Skill 返回的是"上一个任务的结果",而 Agent 以为那是当前任务的结果。
这就是手册里说的"幻觉式错误"——不是 LLM 在胡说,而是 Skill 给了它错误的数据。
手册的铁律是:每个 Skill 调用必须是无状态的(stateless),上下文由 Agent 框架统一持有,Skill 只管"这一次"。
同时,Skills 必须满足幂等性(idempotency):用相同的参数调用两次,结果必须一样。
重构后的写法:
# ✅ 无状态、幂等的 Skill
def search_news(query: str, time_range: str = "24h") -> dict:
"""
完全无状态:所有上下文通过参数传入,不依赖任何外部状态。
幂等:相同参数,相同结果。
"""
result = fetch_news(query=query, time_range=time_range)
return {
"status": "success",
"query": query, # 把输入参数回传,方便 Agent 做溯源
"time_range": time_range,
"data": result
}
注意一个细节:把输入参数回传给 Agent。这不是多余的,这让 Agent 在处理多个并发 Skill 结果时,能准确知道"这个结果是对应哪个查询的"。
这一章的金句:Skill 是一次性的工具,不是长期驻留的服务。每次调用都应该像第一次一样干净。
---
第四章:综合对比 + 实操 Checklist
三个差异的结构化对比
| 维度 | 传统 API 开发思维 | Skills 开发思维 | | 控制权 | 开发者决定何时调用、调用几次 | Planner 决定,开发者通过description 影响决策 |
| 最重要的部分 | 函数体的实现逻辑 | description 字段的表达质量 |
| 错误设计 | 抛 Exception,给人看的错误信息 | 返回结构化自然语言,给 LLM 提供决策依据 |
| 错误目标 | 让开发者能定位 bug | 让 Agent 能决定下一步行动 |
| 状态管理 | 可以维护 session、cache、上下文 | 严格无状态,上下文由框架持有 |
| 幂等性要求 | 视业务场景而定 | 强制要求,相同参数必须相同结果 |
Skills 开发前5问(动手前必须确认)
在你写第一行 Skill 代码之前,问自己这5个问题:
① 我的description 里有没有"触发条件"和"排除条件"?
如果没有,Planner 不知道什么时候该用你的 Skill。
② 我的错误信息,Agent 能从中得到"下一步怎么做"的指导吗?如果只有错误码,没有行动建议,重写。
③ 这个 Skill 依赖任何外部状态吗?如果有 self.xxx、全局变量、session,这是危险信号。
如果不能保证幂等,需要在 description 里明确说明副作用。
这让 Agent 在并发场景里能准确做结果溯源。
---
这套思维不只适用于 Perplexity。任何基于 Function Calling / Tool Use 的 Agent 框架——OpenAI 的 GPT-5 系列、Claude Opus 4.6、Gemini 3.1 Pro——底层逻辑都是一样的。Planner 层的工作方式决定了这三个铁律的普适性。你今天学的不是 Perplexity 的私货,是 Agent 时代的新工程规范。
---
如果你想直接上手验证这三个设计差异,不想折腾本地环境——
>
[api.884819.xyz](https://api.884819.xyz) 已经聚合了 Perplexity、OpenAI、Claude、Gemini 等主流模型的 API 接入,你可以用同一套 Skill 代码,对比不同 Agent 框架对 description、错误信息、无状态设计的实际响应差异。
>
新用户注册即送体验 token,国产模型(Deepseek/千问等)完全免费,没有月租,按量付费。 注册只需用户名+密码,直接用。
>
这是目前验证"Skills 思维"最低成本的方式。→ [直接访问 api.884819.xyz 开始测试](https://api.884819.xyz)
---
最后一句话
本文拆的是"思维层"——三个最根本的设计差异。
但手册里还有一个更硬核的部分没讲:Perplexity 是如何设计 Skills 的"置信度评分"机制的。也就是说,当两个 Skills 都能完成同一个任务时,Agent 凭什么选这个不选那个?Planner 层的选择逻辑是什么?
下一篇,我们拆 Planner 层的决策机制。如果你今天写了一个 Skill 却发现它总是输给另一个 Skill,那篇文章会告诉你为什么。
---
本文由8848AI原创,转载请注明出处。关注8848AI,带你从零开始学AI。#AI开发 #Agent开发 #Perplexity #Skills开发 #FunctionCalling #大模型 #8848AI #AI工程