我按官方文档搭了一遍 Perplexity Agent Skills,踩了 2 个坑文档完全没提

我按着官方文档一步步操作,第一个请求发出去——没报错,也没结果。

不是 400,不是 401,不是任何异常。就是一个干净的 200 响应,body 里返回空数组。

如果你也踩过这种坑,你知道那种感觉有多难受:代码没问题、格式没问题、逻辑没问题,但就是不工作。你开始怀疑自己,开始怀疑文档,最后怀疑整个宇宙。

这篇文章是我在搭完 Perplexity Agent Skills 最小可行模块之后回来写的复盘报告。不是翻译文档,是亲历记录——包括文档里没说清楚、甚至完全没提的部分。

---

一、为什么要照着 Perplexity 的手册自己搭一遍?

Perplexity 的 Agent Skills 功能在国内讨论度相当低。大多数开发者的注意力在 OpenAI 的 Function Calling、Claude 的 Tool Use,或者 LangChain 的 Agent 框架上。Perplexity 更多时候被当成"联网搜索工具"来用,而不是一个可以自定义工具的 Agent 平台。

但我最近在研究"工具调用型 Agent"的设计模式时,发现 Perplexity 的 Skill 机制有一个很值得学习的特点:它的 Schema 定义方式非常接近 OpenAI 的 Function Calling 规范,但在执行层做了一些不同的设计决策

这意味着,如果你搞懂了 Perplexity 的 Skill 机制,你对整个"工具调用型 Agent"的理解会更立体——因为你看到了同一个问题的两种解法。

所以我决定:照着官方文档,从零开始搭一个最小可行的 Skill 模块,然后回来如实报告。

---

二、先把概念搞清楚——Agent Skills 到底是什么?

在开始敲代码之前,有必要把这个概念讲清楚。因为"Skill"这个词在不同语境下含义差别很大,容易混淆。

它不是插件(Plugin),不是你在 ChatGPT 里安装的那种第三方扩展;它不是 Prompt 模板,不是你写在 System Prompt 里的角色设定;它是 Agent 的工具函数——一个有明确输入输出定义的可调用单元。

用一个类比来理解:

你(用户)

↓ 发出指令

Agent(大脑)

↓ 判断需要哪个工具

Skill(工具函数)← 这就是我们要搭的东西

↓ 执行具体任务(调用外部 API、处理数据等)

↓ 返回结构化结果

Agent(大脑)

↓ 整合结果,生成回答

你(用户)

Skill 在这条链路里的位置很清晰:它是 Agent 的手,不是 Agent 的脑。Agent 负责决策(调用哪个 Skill、传什么参数),Skill 负责执行(拿到参数、完成操作、返回结果)。

每个 Skill 有三个核心要素:

  • Name:工具的唯一标识,Agent 通过这个名字来调用它
  • Description:自然语言描述,Agent 依赖这个来判断"什么时候该用这个工具"
  • Parameters Schema:JSON Schema 格式定义的输入参数结构

这个结构和 OpenAI 的 Function Calling 高度相似,如果你用过后者,上手会很快。

---

三、最小可行模块的搭建步骤

我选了一个最简单的案例:网页内容摘要 Skill——给定一个 URL,返回该页面的核心内容摘要。

这个案例足够简单(只有一个输入参数),但又足够真实(涉及外部 HTTP 请求),是验证整个调用链是否跑通的理想选择。

3.1 定义 Skill Schema

首先定义这个 Skill 的结构。按照官方文档的规范,JSON Schema 格式如下:

{

"name": "fetch_page_summary",

"description": "Fetch the content of a given URL and return a concise summary of the page. Use this when the user asks about the content of a specific webpage.",

"parameters": {

"type": "object",

"properties": {

"url": {

"type": "string",

"description": "The full URL of the webpage to fetch and summarize, including the protocol (https://)."

}

},

"required": ["url"]

}

}

几个需要注意的细节:

  • description 字段要写得像是在告诉 Agent "什么情况下用这个工具",而不只是描述功能
  • parameters.type 必须是 "object",这是硬性要求
  • required 数组要明确列出必填参数

3.2 实现 Skill 函数体

Skill 的执行逻辑用 Python 实现:

import httpx

from bs4 import BeautifulSoup

def fetch_page_summary(url: str) -> dict:

"""

Fetch webpage content and return structured summary data.

"""

try:

headers = {

"User-Agent": "Mozilla/5.0 (compatible; SkillBot/1.0)"

}

response = httpx.get(url, headers=headers, timeout=10.0, follow_redirects=True)

response.raise_for_status()

soup = BeautifulSoup(response.text, "html.parser")

# 提取标题

title = soup.find("title")

title_text = title.get_text(strip=True) if title else "No title found"

# 提取正文段落(前 5 段)

paragraphs = soup.find_all("p")

content_preview = " ".join(

p.get_text(strip=True) for p in paragraphs[:5] if p.get_text(strip=True)

)

return {

"success": True,

"title": title_text,

"content_preview": content_preview[:500], # 限制长度

"url": url

}

except Exception as e:

return {

"success": False,

"error": str(e),

"url": url

}

3.3 组装 API 请求

把 Skill 定义和实际调用组合起来,发送给 Perplexity API:

import json

import httpx

PERPLEXITY_API_KEY = "your_api_key_here"

BASE_URL = "https://api.perplexity.ai"

def call_agent_with_skill(user_message: str):

skill_definition = {

"name": "fetch_page_summary",

"description": "Fetch the content of a given URL and return a concise summary.",

"parameters": {

"type": "object",

"properties": {

"url": {

"type": "string",

"description": "The full URL to fetch."

}

},

"required": ["url"]

}

}

payload = {

"model": "llama-3.1-sonar-large-128k-online",

"messages": [

{

"role": "user",

"content": user_message

}

],

"tools": [skill_definition] # ← 注意这个字段名

}

headers = {

"Authorization": f"Bearer {PERPLEXITY_API_KEY}",

"Content-Type": "application/json"

}

response = httpx.post(

f"{BASE_URL}/chat/completions",

headers=headers,

json=payload,

timeout=30.0

)

return response.json()

💡 文中的 API 调用示例使用的是统一接入层思路。
如果你想用同一套代码同时测试 Perplexity、Claude Sonnet 4.6、GPT-5.1 的 Skill 响应差异,推荐通过 [api.884819.xyz](https://api.884819.xyz) 接入——一个 Key 管多个模型,省去反复配置环境的麻烦。特别适合这种横向对比实验,新用户注册即送体验 token。

---

四、我踩的 2 个坑,文档里完全没提

好,现在进入这篇文章最核心的部分。

上面的代码如果你照着敲,大概率会遇到和我一样的问题。以下是两个让我各自排查了不少时间的坑,文档里没有任何提示。

坑一:认证头部字段的格式问题

现象:请求返回 200,但 choices 数组为空,或者 tool_calls 字段根本不存在。

我当时的第一反应是:Schema 定义有问题。于是我把 Schema 翻来覆去改了好几遍,加描述、改参数名、调整 required 字段——没用。

然后我开始怀疑是模型的问题,换了不同的 sonar 模型——还是没用。

排查了将近 40 分钟之后,我偶然把请求头打印出来仔细看了一眼,才发现问题所在:

文档示例
Authorization: Bearer pplx-xxxxxxxxxxxxxxxx
实际要求Bearer 和 token 之间必须是单个空格,且 token 前后不能有任何多余字符(包括换行符)。

听起来很基础对吗?但问题出在我从文档里复制 API Key 的时候,复制到了末尾的一个不可见字符(可能是零宽空格或者换行符)。这导致认证头部格式错误,但 Perplexity 的 API 并不返回 401——它返回 200,但静默地忽略了 tool_calls。

修正前
# 从文档直接复制粘贴的 key,末尾可能带不可见字符

PERPLEXITY_API_KEY = "pplx-xxxxxxxxxxxxxxxx\n" # 肉眼看不出来

headers = {

"Authorization": f"Bearer {PERPLEXITY_API_KEY}",

...

}

修正后
# 显式 strip() 清理不可见字符

PERPLEXITY_API_KEY = "pplx-xxxxxxxxxxxxxxxx".strip()

headers = {

"Authorization": f"Bearer {PERPLEXITY_API_KEY}",

...

}

⚠️ 教训:任何从文档或环境变量读取的 API Key,都要加 .strip() 清理。这不是 Perplexity 独有的问题,几乎所有 API 都可能踩这个坑——只是大多数 API 会返回 401,而不是静默失败。

坑二:Schema 字段类型约束比文档描述的更严格

现象:Skill 被正确调用了(tool_calls 字段存在),但 Agent 传过来的参数类型不对,函数执行后返回空结果或报错。

文档里对 parameters 的描述相当简略,只说"遵循 JSON Schema 规范"。但实际上,Perplexity 在解析 Schema 时有一个未文档化的行为

如果你的参数 Schema 中定义了 "type": "string",但 Agent 在某些情况下会把数字类型的内容(比如纯数字的 URL 参数)以 integer 类型传入,而不做自动类型转换。

更关键的是:类型不匹配不会抛出异常,也不会有任何警告——函数会被调用,但参数值是 null

复现方式很简单:把你的 Skill 参数定义为 "type": "string",然后让 Agent 处理一个包含纯数字内容的请求。

排查过程

我在函数入口加了日志:

def fetch_page_summary(url: str) -> dict:

print(f"[DEBUG] Received url: {repr(url)}, type: {type(url)}")

# ...

输出结果:

[DEBUG] Received url: None, type: 
urlNone。但我的 Schema 里明明把 url 列在了 required 里。 根本原因:当 Agent 传入的参数值与 Schema 定义的类型不匹配时,Perplexity 的参数绑定层会把该参数设为 None,而不是报错。required 字段在这种情况下不会触发校验。 解法:在函数入口做显式类型检查和防御性编程:
def fetch_page_summary(url) -> dict:  # 注意:不加类型注解,避免 Python 层面的类型强制

# 防御性类型检查

if url is None:

return {"success": False, "error": "url parameter is None — possible type mismatch in schema"}

url = str(url).strip() # 强制转换为字符串并清理

if not url.startswith(("http://", "https://")):

return {"success": False, "error": f"Invalid URL format: {url}"}

# 后续正常逻辑...

同时,在 Schema 描述里加上更明确的格式约束:

"url": {

"type": "string",

"description": "The complete URL as a string, must start with http:// or https://. Example: 'https://example.com/article'",

"pattern": "^https?://"

}

加上 pattern 字段之后,Agent 在生成参数时会更倾向于按字符串格式输出,类型不匹配的情况明显减少。

⚠️ 教训:不要相信 required 字段会帮你做类型校验。在 Skill 函数里,永远要做防御性编程——假设任何参数都可能是 None 或错误类型。

---

五、跑通之后能干什么?延伸思路 & 局限性

把两个坑踩完、代码跑通之后,我冷静下来想了想这套机制的边界在哪里。

适合用 Skill 做的事

轻量工具封装:把一个有明确输入输出的外部 API 包装成 Skill,让 Agent 能自主决定何时调用。比如天气查询、汇率转换、数据库简单查询——这类"一问一答"型工具特别适合。 快速原型验证:当你有一个 Agent 产品想法,想快速验证"让 AI 自主调用工具"这条路是否可行,Skill 机制是最低成本的试验场。从想法到跑通,一个下午就够。 2 个可以直接复用的扩展方向

1. 把今天这个网页摘要 Skill 扩展成"研究助手":增加一个 search_and_summarize Skill,输入关键词,自动搜索多个页面并汇总——这是一个完整的信息聚合 Agent 的雏形。

2. 接入内部知识库:把公司内部文档系统包装成 Skill,让 Agent 能自主检索内部知识回答员工问题——比直接 RAG 更灵活,因为 Agent 可以决定"要不要查"以及"查什么"。

不适合用 Skill 做的事

复杂状态管理:Skill 是无状态的单次调用,如果你的任务需要跨多轮对话保持状态(比如"记住上次搜索的结果,基于它继续"),Skill 机制本身不提供这个能力,需要你在外层自己管理。 高并发生产场景:这套机制目前更适合低频调用的场景。如果你需要高并发、高可靠性的工具调用,需要考虑更成熟的 Agent 框架(比如 LangGraph、AutoGen),而不是直接用 Perplexity 的 Skill API。
这套机制值得学,但别高估它——它的价值在于让你快速验证想法,而不是直接上生产。

---

写在最后

这篇文章记录的是一次真实的"照文档搭建 → 遇到问题 → 排查解决"的完整过程。两个坑都不复杂,但在当时都让我卡了一段时间,因为它们的共同特点是静默失败——没有报错,只有空结果。

如果这篇文章帮你节省了 40 分钟的排查时间,那它的目的就达到了。

测试环境说明:本文代码基于 Perplexity API(llama-3.1-sonar-large-128k-online 模型)测试,Python 3.11,httpx 0.27,BeautifulSoup4 4.12。API 行为可能随版本更新而变化,建议以官方文档为准。

---

下一篇我打算做一件更有意思的事: 把今天这个 Skill 模块接进一个多步骤 Agent,看看当工具调用链超过 3 层之后,Perplexity 和 Claude Sonnet 4.6 的行为差异有多大——结果出乎我意料。 关注不丢,下周见。

---

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

#AI教程 #AgentSkills #工具调用 #Perplexity #8848AI #AI开发 #LLM实战 #Agent开发