你以为学会调API就学会了Agent开发?Perplexity的手册第一页就在打这个脸
本文最后更新于 2026-05-10,文章内容可能已经过时。
你以为学会调API就学会了Agent开发?Perplexity的手册第一页就在打这个脸
"不就是把LLM包一层,加几个工具调用吗?"
如果你也这么想过,那你和我当初犯了一样的错误。
三个月前,我在做一个基于Agent的客服系统。功能跑通了,演示效果很好,单轮对话完全没问题。但一上线,用户一旦进入多轮对话,系统就开始出现诡异的行为:前一步查到的订单信息,下一步就"忘了";用户说"就用刚才那个地址",Agent一脸茫然;错误信息直接透传给用户,像极了一个没有情商的程序员在和客户说话。
代码没有报错,逻辑"看起来"没问题。但这个系统是一颗定时炸弹。
直到我读到Perplexity发布的Agent Skills开发手册,第一章第一节就有这样一句话:
"Skills require a different developer mindset than traditional API integrations."
这句话我扫了一眼就跳过去了。直到系统在生产环境炸了第三次,我才回过头来认真读它。
这篇文章,就是我把这句话读懂之后,想写下来的东西。
---
第一章:为什么"能跑"不等于"对了"
先来看一个典型的"伪Skill"。
假设你要写一个"查询用户订单状态"的功能模块,用传统API思维写出来大概是这样:
# 传统API思维写法
def get_order_status(order_id: str) -> dict:
"""
参数:order_id - 必须是标准格式的订单ID
返回:订单状态字典
"""
if not order_id or not order_id.startswith("ORD-"):
raise ValueError("Invalid order_id format")
result = db.query(f"SELECT status FROM orders WHERE id = '{order_id}'")
if not result:
raise Exception("Order not found")
return {"status": result[0]["status"]}
这段代码本身没有问题。如果是一个后端接口,它很合格。
但把它包进Agent的Skills系统里,它就是一颗定时炸弹。
为什么?因为用户不会说"请查询ORD-20240315-001的状态"。用户会说:"我刚下的那个单发货了吗?"或者"上周买的那个,到哪了?"
这段代码在简单场景下能跑——只要上游的意图解析恰好提取出了正确的order_id。但一旦上下文稍微复杂,它就会以各种你预料不到的方式失败,而且失败信息对Agent来说毫无价值,只是一个冰冷的Exception。
这就是Perplexity手册那句话的真正含义:不是说你的代码写错了,而是说你的思维框架用错了。
---
第二章:差异一——输入不是参数,是"意图的碎片"
传统API的输入逻辑很清晰:你定义参数,调用方传参数,参数不符合格式就报错,边界清晰,责任分明。
但Skills的输入来自哪里?来自用户自然语言被LLM解析之后的"意图碎片"。
同一句话,在不同上下文里,语义完全不同。"查一下我的订单"——
- 如果用户刚刚提到了一个具体订单号,这句话指向那个订单
- 如果用户没有提到订单号,这句话需要先查用户的最近订单列表
- 如果用户处于投诉流程中,这句话可能隐含"我要看证据"的意图
Perplexity手册在Skills输入规范部分明确指出,Skills的输入设计必须考虑意图的不完整性。一个设计良好的Skill,不应该在输入不完整时直接抛出错误,而应该能够:
1. 识别输入的完整程度:这个意图片段是否足够我执行任务?
2. 主动请求补全:如果不够,以结构化的方式告诉Agent"我还需要什么"
3. 在模糊情况下给出最优猜测:并附上置信度,让Agent决定是否需要二次确认
用Skills思维重写上面那个例子:
# Skills思维写法
from dataclasses import dataclass
from typing import Optional
@dataclass
class SkillInput:
order_id: Optional[str] = None
user_id: Optional[str] = None
intent_context: Optional[str] = None # 原始意图上下文
@dataclass
class SkillOutput:
success: bool
data: Optional[dict] = None
confidence: float = 1.0 # 置信度信号
needs_clarification: Optional[str] = None # 需要补全的信息
fallback_action: Optional[str] = None # 失败降级路径
error_type: Optional[str] = None # 结构化错误类型
def get_order_status(skill_input: SkillInput) -> SkillOutput:
# 意图推断层:不是参数校验,而是意图补全
order_id = skill_input.order_id
if not order_id and skill_input.user_id:
# 没有订单ID但有用户ID,尝试获取最近订单
recent_orders = db.get_recent_orders(skill_input.user_id, limit=3)
if len(recent_orders) == 1:
order_id = recent_orders[0]["id"]
elif len(recent_orders) > 1:
# 有多个候选,需要澄清
return SkillOutput(
success=False,
needs_clarification="multiple_orders_found",
data={"candidates": recent_orders},
confidence=0.0
)
if not order_id:
return SkillOutput(
success=False,
needs_clarification="order_id_required",
fallback_action="ask_user_for_order_id",
confidence=0.0
)
# 执行查询
result = db.query_order(order_id)
if not result:
return SkillOutput(
success=False,
error_type="order_not_found",
fallback_action="suggest_check_order_list",
confidence=1.0 # 确定性失败,置信度高
)
return SkillOutput(
success=True,
data={"status": result["status"], "order_id": order_id},
confidence=1.0
)
看到差别了吗?传统API是"参数校验层",Skills是"意图推断层"。 前者问的是"你给的对不对",后者问的是"你想要什么,我能不能帮你找到"。
---
第三章:差异二——输出不是返回值,是"下一步行动的燃料"
传统API的输出是终点。调用方拿到结果,渲染给用户,流程结束。
Skills的输出是中间站。它的质量,直接决定了Agent下一步能不能做出正确决策。
这个差异非常反直觉,因为从代码层面看,两者都是"函数返回了一个值"。但返回值的语义完全不同。
Perplexity手册在输出规范部分强调了两个必须携带的信号:
信号一:置信度(Confidence)Skills的输出必须让Agent知道"这个结果有多可靠"。一个查询成功但结果可能不是用户想要的(比如查到了多个候选),和一个查询成功且结果高度匹配的,在Agent决策层面是完全不同的。
如果你只返回{"status": "shipped"},Agent不知道这是"精确匹配的结果"还是"我猜你问的是这个"。
当Skill失败时,它不应该只说"我失败了",而应该告诉Agent"你接下来可以怎么做"。
这是Skills和传统API最本质的输出差异:传统API的错误是给人看的,Skills的错误是给Agent用的。
一个好的Skill输出,是Agent的行动说明书,不是人类可读的报告。
---
第四章:差异三——错误不是异常,是"Agent的学习信号"
这是三个差异里最反直觉的一个。
传统开发的错误处理逻辑:报错 → 捕获 → 告诉用户"出错了"。try-catch是我们的本能反应。
但在Skills开发里,这个本能是危险的。
为什么?因为Agent需要从错误里获取信息,来决定下一步怎么规划。如果你把所有错误都捕获成一个统一的"出错了",Agent就失去了重新规划的依据,只能原地停转,或者做出错误的决策。
Perplexity手册把Skills里的错误分成两类:
可恢复错误(Recoverable Errors)这类错误意味着Agent可以换一种方式重试,或者请求用户补充信息:
- 输入不完整:需要更多上下文
- 外部服务暂时不可用:可以稍后重试
- 权限不足:可以引导用户授权
这类错误意味着这条路走不通,Agent需要彻底换一个方案:
- 用户请求的资源根本不存在
- 操作违反了业务规则
- 需要人工介入的情况
# 错误的错误处理方式(传统API思维)
def get_order_status_bad(order_id: str):
try:
result = db.query_order(order_id)
return result
except Exception as e:
return {"error": "查询失败,请稍后重试"} # ❌ Agent拿到这个完全不知道该怎么办
正确的错误处理方式(Skills思维)
def get_order_status_good(order_id: str) -> SkillOutput:
try:
result = db.query_order(order_id)
return SkillOutput(success=True, data=result, confidence=1.0)
except OrderNotFoundError:
return SkillOutput(
success=False,
error_type="terminal.order_not_found", # ✅ 终止错误,Agent换方案
fallback_action="suggest_order_list_check"
)
except DatabaseTimeoutError:
return SkillOutput(
success=False,
error_type="recoverable.service_timeout", # ✅ 可恢复错误,Agent可重试
fallback_action="retry_after_2s",
confidence=0.0
)
except PermissionDeniedError:
return SkillOutput(
success=False,
error_type="recoverable.permission_denied", # ✅ 可恢复,引导授权
fallback_action="request_user_authorization"
)
try-catch思维在Agent里危险,不是因为它捕获了错误,而是因为它把有价值的错误信息变成了无意义的噪音。
---
第五章:三个差异合在一起,意味着什么?
把三个差异收拢来看:
传统API思维 Skills思维
─────────────────────────────────────────
输入层:参数校验 → 意图推断
输出层:结果返回 → 行动燃料
错误层:异常捕获 → 学习信号
这不是代码风格的差异,这是因果逻辑的差异。
传统API的因果链是:输入 → 处理 → 输出,线性的,封闭的。
Skills的因果链是:意图 → 推断 → 执行 → 信号 → Agent重规划,循环的,开放的。
Skills开发自测清单
在你开始写下一个Skills模块之前,用这5条标准检查自己:
- [ ] 输入层:我的函数在输入不完整时,是报错还是尝试推断?
- [ ] 置信度:我的输出里,Agent能知道这个结果有多可靠吗?
- [ ] 降级路径:我的输出里,包含了"如果这不对,下一步可以做什么"吗?
- [ ] 错误分类:我的错误处理,区分了"可恢复"和"终止"两种类型吗?
- [ ] 信息密度:我的错误信息,是给人看的还是给Agent用的?
如果有两条以上回答"否",你写的是API,不是Skill。
最小可行Skills代码骨架
from dataclasses import dataclass, field
from typing import Optional, Any
from enum import Enum
class ErrorType(Enum):
RECOVERABLE_TIMEOUT = "recoverable.timeout"
RECOVERABLE_PERMISSION = "recoverable.permission_denied"
RECOVERABLE_INCOMPLETE_INPUT = "recoverable.incomplete_input"
TERMINAL_NOT_FOUND = "terminal.not_found"
TERMINAL_BUSINESS_RULE = "terminal.business_rule_violation"
@dataclass
class SkillInput:
"""意图推断层的输入结构"""
raw_intent: str # 原始意图描述
structured_params: dict = field(default_factory=dict) # 已解析的参数
context: dict = field(default_factory=dict) # 上下文信息
user_id: Optional[str] = None
@dataclass
class SkillOutput:
"""行动燃料的输出结构"""
success: bool
data: Optional[Any] = None
confidence: float = 1.0
error_type: Optional[ErrorType] = None
needs_clarification: Optional[str] = None
fallback_action: Optional[str] = None
metadata: dict = field(default_factory=dict)
def my_skill(skill_input: SkillInput) -> SkillOutput:
"""
Skills函数模板
原则:
1. 永远不要直接raise,永远return SkillOutput
2. 失败时必须携带error_type和fallback_action
3. 成功时必须携带confidence
"""
# Step 1: 意图推断层
# 从不完整的输入中尽量推断完整意图
inferred_params = _infer_params(skill_input)
if inferred_params.get("needs_clarification"):
return SkillOutput(
success=False,
error_type=ErrorType.RECOVERABLE_INCOMPLETE_INPUT,
needs_clarification=inferred_params["needs_clarification"],
confidence=0.0
)
# Step 2: 执行层
try:
result = _execute(inferred_params)
return SkillOutput(
success=True,
data=result,
confidence=_calculate_confidence(result, skill_input)
)
except TimeoutError:
return SkillOutput(
success=False,
error_type=ErrorType.RECOVERABLE_TIMEOUT,
fallback_action="retry_after_delay"
)
except NotFoundError:
return SkillOutput(
success=False,
error_type=ErrorType.TERMINAL_NOT_FOUND,
fallback_action="suggest_alternatives"
)
代码示例里调用的模型接口,使用的是统一格式的中转层。如果你想在自己的Skills项目里复用这套结构,不想被单一平台的API格式绑定,可以试试 [api.884819.xyz](https://api.884819.xyz),兼容OpenAI格式,意图推断层和错误信号层都能直接套用,省去格式转换的心智负担。新用户注册即送体验token,国产模型(Deepseek/千问等)完全免费,没有月租。
---
结语:这套思维不只属于Perplexity
Perplexity的Skills手册是一份非常务实的文档。它没有讲大道理,而是在每一个规范背后,都隐藏着"如果你不这样做,会在哪里出问题"的工程经验。
这3个差异——意图推断 vs 参数校验、行动燃料 vs 结果返回、学习信号 vs 异常捕获——不是Perplexity专有的设计哲学。它们是所有Agent开发的底层操作系统。
不管你用的是LangChain、AutoGen、还是自己搭的框架,只要你在写Agent的功能模块,这套思维都适用。
用API思维写出来的Agent,能跑,但会在你看不见的地方悄悄烂掉。
现在你知道它在哪里烂了。
---
📌 下一篇预告
Perplexity手册里还有一个细节很少人注意:它对Skills的上下文窗口管理有一套非常具体的规范,而这套规范和LangChain的做法,在一个关键节点上是矛盾的。
到底是Perplexity的设计更接近Agent的真实运行逻辑,还是LangChain的抽象更合理?
下一篇我们来拆这个矛盾——两个主流框架在同一个问题上给出了截然相反的答案,而真相可能比你想象的更微妙。
---
本文由8848AI原创,转载请注明出处。关注8848AI,带你从零开始学AI。#AI开发 #Agent开发 #Perplexity #LLM #Skills开发 #8848AI #人工智能 #开发者工具