我以为我在写函数,结果我在写菜单——Perplexity Agent Skills 手册实战拆解
本文最后更新于 2026-05-10,文章内容可能已经过时。
我以为我在写函数,结果我在写菜单——Perplexity Agent Skills 手册实战拆解
有一个瞬间,让我在键盘前愣了将近三分钟。
那是我第一次按照 Perplexity Agent Skills 手册把一个"联网搜索摘要"技能跑通之后。代码没报错,输出也对,但我突然意识到一件事:这整个过程里,我从来没有"调用"过这个函数。
我写了它,测试了它,但调用它的,是模型。
如果你有普通编程经验,这句话读起来可能有点绕。但这个绕,正是 Agent 开发和传统开发之间真正的分水岭——不是什么框架选型,不是什么向量数据库,就是这一句:你以为你在写函数,实际上你在声明意图。
本文只做一件事:把这个反直觉的瞬间,还原给你看。
---
为什么选 Perplexity Agent Skills 手册来练手?
市面上 Agent 框架不少,LangChain、AutoGPT、OpenAI Assistants……但这些框架普遍有一个问题:层层抽象把最核心的概念藏得太深,你跑通了 demo,但不知道为什么它能跑通。
Perplexity 的 Agent Skills 规范相对克制。它聚焦的是单个技能模块的最小单元:一个 Manifest(技能声明)、一个执行函数、一个输出格式。没有多余的包装,概念暴露得很裸。
这正是拆解它的价值所在。
本文的对比基准很简单:你会写普通 Python 函数。全文以"和普通函数调用的差异"为主线,每一步都问同一个问题:如果这是普通函数,我会怎么写?Agent Skill 里,有什么不一样?
---
第一步:环境准备,让代码先跑起来
在讲任何原理之前,先把东西跑通。这是最重要的原则。
目录结构
my_skill/
├── requirements.txt
├── manifest.json # 技能声明,给模型看的
├── skill.py # 执行函数,真正干活的
└── test_skill.py # 本地测试用
四个文件,不多不少。
依赖安装
# requirements.txt
requests==2.31.0
openai==1.30.0 # 用于兼容格式调用
python-dotenv==1.0.0
pip install -r requirements.txt
Manifest:给模型看的"菜单"
这是第一个反直觉点,但先不解释,先看代码:
// manifest.json
{
"name": "web_search_summary",
"description": "Search the web for a given query and return a concise summary of the top results. Use this skill when the user asks about recent events, current information, or anything that requires up-to-date knowledge.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to look up. Should be a clear, specific question or topic."
},
"max_results": {
"type": "integer",
"description": "Maximum number of search results to include in the summary. Default is 3.",
"default": 3
}
},
"required": ["query"]
}
}
执行函数
# skill.py
import requests
import os
from dotenv import load_dotenv
load_dotenv()
def web_search_summary(query: str, max_results: int = 3) -> dict:
"""
执行联网搜索并返回摘要
注意:返回格式是给模型读的,不是给代码取 key 的
"""
api_key = os.getenv("PERPLEXITY_API_KEY")
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"model": "llama-3.1-sonar-small-128k-online",
"messages": [
{
"role": "user",
"content": f"Search for: {query}. Provide a concise summary of the top {max_results} results."
}
]
}
try:
response = requests.post(
"https://api.perplexity.ai/chat/completions",
headers=headers,
json=payload,
timeout=30
)
response.raise_for_status()
result = response.json()
return {
"status": "success",
"query": query,
"summary": result["choices"][0]["message"]["content"],
"instruction": "The above summary is based on current web search results. Use this information to answer the user's question accurately."
}
except requests.exceptions.Timeout:
return {
"status": "error",
"error_type": "timeout",
"message": "The search request timed out after 30 seconds.",
"instruction": "Inform the user that the search is temporarily unavailable and suggest they try again or rephrase their query."
}
except Exception as e:
return {
"status": "error",
"error_type": "unknown",
"message": str(e),
"instruction": "An unexpected error occurred. Apologize to the user and suggest trying a different search query."
}
本地测试
# test_skill.py
from skill import web_search_summary
import json
result = web_search_summary("2024年大语言模型最新进展", max_results=3)
print(json.dumps(result, ensure_ascii=False, indent=2))
跑通了?好。现在我们来拆解为什么这段代码里有三个地方,和你的直觉完全相反。
---
如果你不想在 API Key 申请和额度管理上花时间,可以直接用聚合接口跳过这步——[api.884819.xyz](https://api.884819.xyz) 统一接入了包括 Perplexity 在内的主流模型 API,格式兼容,按量计费,本文的代码示例直接换上去就能跑。新用户注册即送体验 token,国产模型(Deepseek / 千问等)完全免费。
---
第二步:逐帧拆解三个反直觉点
反直觉点 #1:你不调用技能,你在"描述"技能
普通函数的逻辑是这样的:
你 → call(function) → 得到结果
Agent Skill 的逻辑是这样的:
你写 Manifest → LLM 读 Manifest → LLM 决定要不要调用 → LLM 调用 → 你得到结果
控制权在中间那一步,悄悄反转了。
你有没有注意到 manifest.json 里 description 字段的写法?
"description": "Use this skill when the user asks about recent events, current information, or anything that requires up-to-date knowledge."
这不是给开发者看的注释。这是给 LLM 看的调用说明书。LLM 会读这段话,然后决定:用户的这个问题,要不要触发这个技能?
这意味着你写的不是逻辑,是意图的声明。你在告诉模型"这个工具能干什么、什么时候该用它",而不是"现在执行这段代码"。
这是 Agent 开发和传统编程之间最大的认知颠覆。普通程序员的第一直觉是"我来控制执行流",但在 Agent 语境里,你要学会把这个控制权交出去。
反直觉点 #2:返回值不是给代码用的,是给模型读的
看这段代码:
return {
"status": "success",
"query": query,
"summary": result["choices"][0]["message"]["content"],
"instruction": "The above summary is based on current web search results. Use this information to answer the user's question accurately."
}
注意 instruction 这个字段。
如果这是普通函数,你绝对不会在返回值里放一句"接下来你应该怎么用这个结果"。那是废话。调用方自己知道怎么用。
但在 Agent Skill 里,消费这个返回值的不是你的代码,是 LLM。
LLM 会把你的返回值当作上下文,继续生成回复。如果你的返回格式含糊、结构混乱,模型不会报错——它会悄悄"理解"成另一个意思。这种 bug 最难排查,因为程序运行正常,只是输出莫名其妙。
所以这里有一个新的设计原则:返回值要写成"模型能理解的自然语言结构"。字段名要语义清晰,关键信息要有上下文说明,甚至可以直接用 instruction 字段告诉模型"拿到这个结果之后该怎么办"。
反直觉点 #3:错误处理要"说给模型听"
传统错误处理:
except Exception as e:
raise RuntimeError(f"Search failed: {e}")
# 或者 return None,让调用方处理
Agent Skill 错误处理:
except Exception as e:
return {
"status": "error",
"message": str(e),
"instruction": "An unexpected error occurred. Apologize to the user and suggest trying a different search query."
}
区别在哪?传统的 raise 是给运行时处理的,return None 是给调用方代码处理的。但在 Agent 里,错误发生之后,下一个"决策者"是 LLM。
如果你只返回 {"status": "error", "message": "timeout"},LLM 拿到这个信息,不知道该怎么办,很可能会原地打转,或者给用户一个莫名其妙的回复。
你需要在错误信息里告诉模型:接下来应该怎么做。这不是多余的,这是 Agent 错误处理的标配。
---
第三步:一张表格,把三个反直觉点结构化
| 维度 | 普通函数 | Agent Skill | | 调用者 | 你的代码 | LLM | | 返回值消费者 | 你的代码(取 key) | LLM(读语义) | | 错误处理对象 | 运行时 / 调用方代码 | LLM(需要告诉它下一步) | | 调试方式 | 打断点、看堆栈 | 看模型输出、分析 Manifest 描述 | | 核心心智模型 | "我来执行" | "我来声明,模型来决定" |这张表值得截图保存。
改造练习:只改三处
把下面这段普通函数改成 Agent Skill 格式,感受控制权的转移:
# 改造前:普通函数
def get_weather(city: str) -> dict:
data = fetch_weather_api(city)
return {"temp": data["temp"], "condition": data["condition"]}
改造后:Agent Skill(只改三处)
def get_weather(city: str) -> dict:
data = fetch_weather_api(city)
return {
"city": city,
# 改动 1:返回值加语义说明
"current_weather": f"Temperature: {data['temp']}°C, Condition: {data['condition']}",
# 改动 2:加 instruction 字段
"instruction": "Present this weather information naturally in your response."
}
改动 3:manifest.json 的 description 要写清楚"什么时候用这个技能"
"description": "Get current weather for a specific city. Use when user asks about weather conditions."
三处改动,背后是一次思维方式的迁移。
---
第四步:踩坑记录
坑 #1:Manifest 字段缺失
报错现象:Error: Skill validation failed
Missing required field: 'parameters.properties'
Manifest schema validation error at line 1
原因: parameters 里忘了写 properties,直接写了参数名。
修复 diff:
// 修复前(错误)
"parameters": {
"query": {"type": "string"}
}
// 修复后(正确)
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "..."}
},
"required": ["query"]
}
坑 #2:Schema 类型不匹配
报错现象:TypeError: max_results must be integer, got str
Skill execution failed: parameter type mismatch
原因: LLM 有时候会把数字以字符串形式传入,需要在执行函数里做类型转换。
修复:
# 修复前
def web_search_summary(query: str, max_results: int = 3):
...
修复后
def web_search_summary(query: str, max_results = 3):
max_results = int(max_results) # 显式转换,防止类型不匹配
...
坑 #3:输出被截断
报错现象: 模型返回的摘要在某个位置突然中断,没有完整输出。 原因: Perplexity API 默认有max_tokens 限制,长文本会被截断。
修复:
payload = {
"model": "llama-3.1-sonar-small-128k-online",
"messages": [...],
"max_tokens": 1024 # 显式设置,避免默认值截断
}
---
扩展路径:跑通之后,往哪走?
单个技能跑通之后,成长地图大概是这样的:
单技能模块(本文)
↓
多技能编排(让模型在多个 Manifest 里选择)
↓
带状态的 Agent(跨轮次记忆、任务拆解)
↓
接入外部数据库(RAG + Tool Use 组合)
↓
多模型协作(GPT-5.x + Perplexity 在同一个 Agent 里分工)
每一层的核心挑战都不是代码,而是如何把你的意图,写成模型能理解的声明。
如果你接下来想测试多模型编排(比如让 GPT-5.x 和 Perplexity 在同一个 Agent 里协作),建议用统一 API 网关管理密钥,省去反复切换 SDK 的麻烦——[api.884819.xyz](https://api.884819.xyz) 的多模型路由功能正好覆盖这个场景,没有月租,按量计费。
---
最后一句话
会调 API 的人很多。能把控制权交出去的人,才真正进入了 Agent 开发的语境。
"跑通"只是起点,真正值钱的,是建立"意图声明"的思维方式。这套思维不只适用于 Perplexity——OpenAI Function Calling、Claude Tool Use、Gemini Function Declarations,背后是同一套逻辑。学一次,到处用。
你在哪一步感觉最反直觉?欢迎在评论区告诉我。---
下篇预告
>
跑通单个技能之后,下一个问题自然出现了——
>
"如果我有 5 个技能,Agent 怎么决定用哪个?"
>
我们会用同一套 Perplexity 手册,把"技能路由"这一层拆开来看:为什么有时候你明明写了技能,模型就是不调用它?Manifest 里有一个字段,90% 的教程从来不提,但它决定了模型愿不愿意"信任"你的技能。
>
下篇见。
---
本文由8848AI原创,转载请注明出处。关注8848AI,带你从零开始学AI。#AI教程 #AgentDev #Perplexity #AIAgent #Python #大语言模型 #8848AI #AI开发