1. 为什么你的 DeerFlow Agent 总是“答得对但做得乱”
很多人第一次跑通 DeerFlow 之后,都会经历一个相似的阶段:单轮问答挺聪明,一旦让它做“调研 + 整理 + 出图 + 写报告”这种复合任务,输出就开始飘。要么步骤漏了,要么工具乱调,要么同一个坑反复踩。我试过把提示词写得很长去约束它,结果上下文被塞满,效果反而更差。
问题不在模型,而在你缺了一层“工作流封装”。DeerFlow 的 Skills 技能系统就是干这个的:它把“遇到某类任务该按什么步骤做、用哪些工具、参考哪些文档”写成一份 Markdown 手册,Agent 在需要时按需加载,而不是每次都靠你临时叮嘱。这一篇就带你从 SKILL.md 的定义规范,一路走到技能加载与调用链路,最后在本地跑通一个自定义技能,并确认 Agent 真的识别、真的调用。
先明确三个概念,避免后面混淆。工具(Tool)是原子操作,比如web_search、bash、read_file,每个工具干一件事,注册在代码层。技能(Skill)是更高层的编排模板,本质是一份SKILL.md文档,告诉 Agent 先做什么、再做什么、用哪些工具、参考哪些资源。技能系统则是这套文档的发现、加载、缓存、启用/禁用与安全校验机制。
适合谁读:已经跑通 DeerFlow 基础对话、想给 Agent 扩展专属能力的开发者;被“多步骤任务不稳定”困扰的 Agent 玩家;以及想理解渐进式加载(Progressive Loading)这类上下文优化思路的工程师。读完你能自己写一个技能、注册进去、验证它被正确触发。
2. TaoToken 前置准备:给技能调用链路配一个稳定模型入口
技能系统本身不依赖特定模型服务,但技能被触发后,Agent 要读 SKILL.md、要按指令调工具、要做安全扫描,这些都走模型推理。如果你本地模型时好时坏,排查技能问题时你会分不清是“技能没被加载”还是“模型没响应”。所以先把模型入口固定下来,后面排障才干净。
我用 TaoToken 作为统一入口,原因是它兼容 OpenAI 风格的接口,Base URL 和 Key 一填就能用,省去在多个 SDK 之间来回改配置的麻烦。官网入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api (这个不加 UTM,直接填进配置即可)。
你需要准备三件套,这是后面所有配置的基础:
| 配置项 | 值 | 说明 |
|---|---|---|
| Base URL | https://taotoken.net/api | OpenAI 兼容接口前缀 |
| API Key | 在控制台创建 | 形如sk-...,只显示一次 |
| Model ID | 按需选择 | 技能推理建议用能力较强的模型 |
创建 Key 的路径是控制台里的 API Keys 页面:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。如果你还没决定用哪个模型,可以先在模型对话页面试一下:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。
注意:技能的安全扫描(security_scanner)会调用 LLM 做内容分类。如果模型入口不稳定,扫描失败会走保守回退策略——默认 block,导致你保存技能时被拒。所以先把模型连通性确认好,再动技能。
这里要强调一个容易踩的坑:技能系统里的“模型调用”和“工具调用”是两条链路。工具(如read_file)是本地执行的,不经过模型;但“判断该不该加载某个技能”是模型根据 description 做的决策。所以模型入口挂了,表现是“技能明明存在却从不触发”,而不是报错。把 Base URL、Key、Model ID 三件套配好,是后面验证步骤能跑通的前提。
如果你打算长期做 Agent 编码和技能迭代,可以考虑 Coding Plan,额度更划算:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。接入细节可以对照文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
3. 可复制配置:SKILL.md 模板与技能注册
这一节是全文的核心操作区。我们分两步:先写一个能跑的自定义技能,再把它注册进 DeerFlow 并确认配置生效。
3.1 技能目录结构
DeerFlow 的技能放在项目根目录的skills/下,分public/(内置,只读)和custom/(自定义,可编辑,被 gitignore)。我们要动的是custom/:
skills/ ├── public/ # 20 个内置技能,只读 │ └── deep-research/ │ └── SKILL.md └── custom/ # 用户自定义,可编辑 └── my-translator/ └── SKILL.md支持的资源子目录只有四个,这是安全边界,别乱建:references、templates、scripts、assets。
3.2 SKILL.md 完整模板
SKILL.md由两部分组成:YAML Front Matter(---包裹的元数据)+ Markdown 正文。下面是一个可直接复制的翻译技能模板:
--- name: my-translator description: Use this skill when the user asks to translate text between languages. Provides a structured translation workflow with context awareness and style adaptation. version: 1.0.0 author: your-name --- # Translation Skill ## Overview This skill provides a systematic approach to text translation that goes beyond literal translation, preserving tone and domain terminology. ## Workflow ### Step 1: Analyze Source Text - Identify the source language - Understand the context and tone - Note any domain-specific terminology ### Step 2: Translate - Preserve the original meaning and nuance - Adapt cultural references when appropriate - Maintain consistent terminology ### Step 3: Review - Compare with the original for accuracy - Check for natural flow in the target language - Verify technical terms are correctFront Matter 里name和description是必需字段,缺一个整个技能会被跳过。命名规则很严:只允许小写字母、数字和连字符,不能以连字符开头/结尾,不能有连续连字符,最长 64 字符。description不能含尖括号(防止 XML 注入),最长 1024 字符。
3.3 启用状态配置
技能的启用/禁用由extensions_config.json管理,它同时管 MCP 服务器和技能。路径发现顺序是:显式传入的config_path→ 环境变量DEER_FLOW_EXTENSIONS_CONFIG_PATH→backend/extensions_config.json→ 项目根extensions_config.json→ 旧文件名mcp_config.json。
{ "mcpServers": {}, "skills": { "deep-research": { "enabled": true }, "my-translator": { "enabled": true }, "video-generation": { "enabled": false } } }判定逻辑很简单:配置里写了"enabled": false就禁用;没出现在skills字段里的,public和custom默认启用。也就是说,你新建的技能不用手动加进配置,它会自动生效。
3.4 带资源的技能配置
如果技能要引用外部文档或脚本,加上子目录,并在正文里用read_file引用:
## Workflow ### Step 1: Read Report Template Load the report template: `read_file /mnt/skills/custom/my-report-generator/templates/quarterly-report.md` ### Step 2: Generate Report Follow the format guide at: `references/report-format-guide.md` ### Step 3: Export PDF Execute the generation script: ```bash python /mnt/skills/custom/my-report-generator/scripts/generate_pdf.py \ --input /mnt/user-data/workspace/report.md \ --output /mnt/user-data/outputs/report.pdf注意容器内路径是 `/mnt/skills/custom/{name}/...`,和你本地的 `skills/custom/{name}/...` 是映射关系,写引用时用容器路径。 ### 3.5 通过 API 注册与刷新 DeerFlow Gateway 提供了完整的技能管理 REST API: | 接口 | 方法 | 说明 | | --- | --- | --- | | `/api/skills` | GET | 列出所有技能(public + custom) | | `/api/skills/custom` | GET | 列出自定义技能 | | `/api/skills/custom/{name}` | PUT | 编辑自定义技能(含安全扫描) | | `/api/skills/{name}` | PATCH | 更新技能状态(启用/禁用) | | `/api/skills/install` | POST | 从 .skill 归档安装 | 每次状态变更后,API 会调用 `refresh_skills_system_prompt_cache_async()` 刷新系统提示词缓存。这一步很关键,因为 Gateway 和 LangGraph Server 跑在不同进程里,不刷新的话 Agent 感知不到变化。 ```bash # 列出所有技能,确认 my-translator 已被发现 curl -s http://localhost:8000/api/skills | jq '.skills[] | {name, category}' # 禁用某个技能 curl -X PATCH http://localhost:8000/api/skills/video-generation \ -H "Content-Type: application/json" \ -d '{"enabled": false}'4. 验证请求:确认 Agent 真的识别并调用了技能
配置写完不代表生效,必须验证。技能系统的验证分三层:文件层能解析、提示词层被注入、运行时被调用。
4.1 文件层验证
先确认SKILL.md能被正确解析。Front Matter 解析器是自建的轻量级实现,支持三种值格式:简单值、折叠块(>,多行合并为单行)、字面量块(|,保留换行)。解析后name和description缺失就返回None,技能被跳过。
# 检查文件是否存在、Front Matter 是否完整 head -n 10 skills/custom/my-translator/SKILL.md # 通过 API 获取技能内容,验证解析结果 curl -s http://localhost:8000/api/skills/custom/my-translator | jq .如果返回里能看到name和description,说明文件层通过。
4.2 提示词层验证
技能信息通过get_skills_prompt_section()注入到系统提示词的<skill_system>段。每个技能条目包含<name>、<description>(带[built-in]或[custom, editable]标签)、<location>(容器内路径)。
<skill_system> You have access to skills that provide optimized workflows for specific tasks. **Progressive Loading Pattern:** 1. When a user query matches a skill's use case, immediately call `read_file` on the skill's main file using the path attribute provided in the skill tag ... <available_skills> <skill> <name>my-translator</name> <description>Use this skill when the user asks to translate text... [custom, editable]</description> <location>/mnt/skills/custom/my-translator/SKILL.md</location> </skill> </available_skills> </skill_system>你可以打印构建好的系统提示词,确认my-translator出现在<available_skills>里。如果没出现,回到第 3 节检查extensions_config.json和缓存刷新。
4.3 运行时验证
这是最关键的一步。发一条明确匹配 description 的消息,观察 Agent 是否调用read_file加载技能:
curl -X POST http://localhost:8000/api/chat \ -H "Content-Type: application/json" \ -d '{ "message": "帮我把这段英文翻译成中文:The quick brown fox jumps over the lazy dog.", "stream": true }'在返回的工具调用轨迹里,你应该看到类似:
[tool_call] read_file path: /mnt/skills/custom/my-translator/SKILL.md [tool_result] (技能内容加载成功) [assistant] 按技能工作流执行:分析源文本 → 翻译 → 复核...看到read_file指向你的技能路径,就说明整条链路通了。如果 Agent 直接翻译、没读技能,说明 description 没匹配上,或者技能没被注入提示词。
4.4 渐进式加载的 token 对照
验证时顺便理解一下为什么这套机制省上下文。三层加载的 token 消耗差异很大:
| 加载层级 | 内容 | token 消耗 | 触发时机 |
|---|---|---|---|
| 第一层 | name + description + path | ~50 token/技能 | 每次对话 |
| 第二层 | 完整 SKILL.md | 数百~数千 token | Agent 判断需要时 |
| 第三层 | references/scripts 等 | 按需 | 工作流执行中 |
20 个技能在第一层只消耗约 1000 token,全量加载可能要 50000+ token。这就是为什么技能系统能在不炸上下文的前提下扩展能力。
5. 本篇常见错排查:401、local proxy failed 与技能不触发
技能系统的报错往往不在技能本身,而在模型入口或缓存。下面按真实报错逐个拆。
5.1 401 Unauthorized
Error: 401 Unauthorized {"error": {"message": "Invalid API key", "type": "invalid_request_error"}}这是模型入口的 Key 问题,不是技能问题。检查三件套:Base URL 是否为https://taotoken.net/api,Key 是否完整(sk-开头),Model ID 是否拼写正确。技能的安全扫描会调 LLM,Key 错了扫描直接失败,保存技能时会被 block。
# 快速验证 Key 是否可用 curl -s https://taotoken.net/api/models \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" | head5.2 local proxy failed
Error: local proxy failed: connection refused这个报错通常出现在你配置了本地代理但服务没起来,或者 Base URL 指向了本地端口。技能系统本身不需要代理,把 Base URL 直接指向https://taotoken.net/api即可。检查环境变量里有没有残留的HTTP_PROXY/HTTPS_PROXY指向失效地址。
5.3 reading 'choices' 报错
TypeError: Cannot read properties of undefined (reading 'choices')这是响应结构不符合预期,常见原因是 Base URL 少了/api后缀,或者用了非 OpenAI 兼容的路径。确认 Base URL 是https://taotoken.net/api,请求路径是/chat/completions。技能触发时如果模型返回结构异常,Agent 会拿不到决策结果,表现就是“技能不触发”。
5.4 OAuth 相关报错
Error: OAuth token expired or invalid如果你用的是需要 OAuth 的客户端(如某些 CLI 工具),token 过期会导致模型调用失败。重新走一遍授权流程,或者改用 API Key 方式。技能系统不关心你用哪种鉴权,只要模型能通就行。
5.5 技能存在但从不触发
这是最隐蔽的一类。排查顺序:
先确认技能在提示词里:打印系统提示词,看<available_skills>有没有你的技能。没有就检查extensions_config.json是否把它禁用了,或者缓存没刷新。
再确认 description 匹配:description 是触发匹配的唯一依据,Agent 看不到技能正文。如果 description 写得太模糊(比如只写“翻译技能”),模型可能判断不出该在什么时候用。参考内置deep-research的写法,明确列出触发场景。
最后确认模型能力:技能触发是模型决策,能力太弱的模型可能忽略技能列表。换一个推理能力更强的 Model ID 再试。
5.6 安全扫描误 block
ScanResult(decision="block", reason="potential prompt injection detected")编辑自定义技能时,security_scanner.py会用 LLM 对内容分类,三级结果:allow正常保存、warn保存但记录警告、block拒绝保存。如果 LLM 扫描不可用,走保守回退——默认 block。检查技能内容里有没有看起来像系统指令覆盖、提权、数据外泄的表述,改掉再存。
6. 把技能系统用起来:从自定义技能到长期 Agent 工作流
到这里,你已经能写 SKILL.md、注册技能、验证调用、排查常见错。最后说几个实战里真正省事的点。
技能和工具的关系要摆正:技能不替代工具,它是工具之上的编排层。一个 PPT 生成技能可能组合web_search(调研)、write_file(写 JSON)、bash(执行生成脚本)、present_file(展示)多个工具。你写技能时,重点是把“步骤顺序”和“资源引用”写清楚,而不是重复描述工具怎么用。
技能自演化值得开。当skill_evolution.enabled为 true 时,Agent 完成任务后如果满足条件(工具调用 5+ 次、踩过非显而易见的坑、用户纠正后生效、发现可复用工作流),会考虑自动创建或改进技能。Agent 生成的技能同样要过安全扫描,不用担心它乱写。
版本追踪别忘了。每次自定义技能被改,skills/custom/.history/{name}.jsonl会记录一条 JSONL,包含时间戳、操作类型、操作者、修改前后内容、扫描结果。出问题时可以回滚。
如果你要把这套技能系统接到长期编码或 Agent 工作流里,Coding Plan 的额度更适合持续迭代:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。需要查接口细节时对照文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。想先验证模型对技能 description 的匹配能力,去模型对话页面发几条测试消息最快:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。
下一篇会拆 Sub-Agents 子智能体,讲 Lead Agent 怎么通过task工具把复杂任务委派给多个子智能体并行执行。技能系统是“单个 Agent 怎么把一件事做规范”,子智能体是“多个 Agent 怎么把一堆事并行做完”,两者配合起来才是完整的编排能力。