本文最后更新于 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.jsondescription 字段的写法?

"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开发