1. 从一次“技能不触发”的排查说起Nanobot Skills 与 Function Calling 到底怎么串起来如果你正在读 Nanobot 的源码大概率已经翻过SkillsLoader这个类。它做的事情看起来不复杂扫描目录、读SKILL.md、解析 frontmatter、拼一段 XML 塞进 system prompt。但真正把 Agent 跑起来之后很多人会卡在同一个地方——技能明明写好了模型就是不调用。我试过在一个本地 Agent 项目里挂三个 Skill结果只有memory被触发另外两个pdf-tools和git-helper从头到尾没动静。翻日志才发现问题不在SkillsLoader而在于我没有把 Skill 的加载动作真正接到 Function Calling 的 tool 注册表里。Nanobot 的SkillsLoader只负责“发现和描述技能”真正让模型能“加载技能”的那一步需要你在 ToolRegistry 里注册一个load_skill工具并且把build_skills_summary()的输出放进 system prompt。这两件事缺一不可。这篇文章就围绕这条链路展开从SKILL.md的 frontmatter 定义到SkillsLoader如何生成技能摘要再到 Function Calling 如何注册load_skill工具、模型如何触发、tool_result如何把完整技能内容注入上下文。最后我会用 TaoToken 的统一 Key 和 API 通道把整条链路在本地跑通一次给出可复制的配置片段和验证步骤。适合已经在写 Agent、想搞清楚 Skills 和 Function Calling 边界的人。核心检索词先摆出来Nanobot Skills 是一套基于SKILL.md的技能加载机制它本身不创造新能力而是把 Function Calling 组织成“按需加载文档 按文档执行工具”的两段式流程。适合谁适合正在做单 Agent 架构、又不想上 Multi-Agent 复杂度的开发者。2. TaoToken 前置统一 Key 与 API 通道让 Function Calling 调试不再被网络问题打断在拆源码之前先把运行环境搭好。Skills 的触发链路里模型调用是最容易出问题的一环——不是逻辑错而是请求本身发不出去或者返回格式不对。用 TaoToken 的好处是它提供统一的 Key 和 API 通道OpenAI 兼容格式你在本地调试 Function Calling 时不用来回切换不同厂商的 SDK。TaoToken 的官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 注意这个地址后面不加 UTM 参数。你需要在控制台创建一个 API Key然后把它写进环境变量。具体操作路径先打开 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 生成 Key再参考 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里的接入说明确认请求格式。如果你用的是 Claude Code 这类工具可以看 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 的配置方式如果是长期跑编码 AgentCoding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 里有套餐说明。环境变量这样设export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api然后在 Python 里用 OpenAI SDK 初始化客户端from openai import OpenAI import os client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], ) resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: ping}], ) print(resp.choices[0].message.content)这一步能跑通说明你的 Key 和通道没问题。后面 Skills 触发失败时就可以排除网络层直接看 tool 注册和 prompt 拼接。这里有个细节TaoToken 的 base_url 末尾不要带斜杠SDK 会自己拼/chat/completions带了斜杠反而可能 404。注意不要把 Key 硬编码进SKILL.md或任何会提交到 Git 的文件。Skill 文档是给模型读的不是放密钥的地方。需要环境变量的 Skill用requires.env声明让SkillsLoader去校验。3. 可复制配置SKILL.md 定义、SkillsLoader 摘要与 load_skill 工具注册这一节是整条链路的核心。我会给出三个可复制的片段一个完整的SKILL.md、SkillsLoader生成摘要的调用方式、以及load_skill工具的 JSON Schema 注册。先看SKILL.md。Nanobot 的 frontmatter 用 YAMLname是唯一标识description决定触发时机always控制是否常驻。下面这个例子是一个“日志分析”技能--- name: log-analyzer description: 分析本地日志文件提取错误模式并生成摘要。当用户提到日志报错error log排查时触发。 always: false metadata: | { nanobot: { requires: { bins: [grep], env: [] } } } --- # Log Analyzer ## 使用场景 当用户需要分析日志文件中的错误模式时使用本技能。 ## 步骤 1. 用 exec 工具运行 grep -iE error|fail|exception logfile 提取错误行。 2. 统计每种错误出现的次数grep -iE error|fail logfile | sort | uniq -c | sort -rn。 3. 把结果整理成表格返回给用户。 ## 注意事项 - 日志文件可能很大先确认文件大小再决定是否全量读取。 - 如果用户没有指定文件路径先问清楚。这个文件放在workspace/skills/log-analyzer/SKILL.md。SkillsLoader初始化时传入workspace路径它会自动扫描workspace/skills/下的子目录。接下来是SkillsLoader的调用。Nanobot 里build_skills_summary()返回的是 XML 格式的技能索引这段内容要放进 system promptfrom pathlib import Path from nanobot.skills import SkillsLoader loader SkillsLoader(workspacePath(./workspace)) summary loader.build_skills_summary() print(summary)输出大概是这样skills skill availabletrue namelog-analyzer/name description分析本地日志文件提取错误模式并生成摘要。当用户提到日志报错error log排查时触发。/description location/abs/path/workspace/skills/log-analyzer/SKILL.md/location /skill /skills注意available属性它由_check_requirements决定。如果grep不在 PATH 里这里会变成false并且多一个requiresCLI: grep/requires节点。模型看到availablefalse就不会去加载它。然后是关键一步注册load_skill工具。SkillsLoader本身不注册工具它只提供load_skill(name)方法。你需要在 ToolRegistry 里加一个 Function Calling 定义LOAD_SKILL_TOOL { type: function, function: { name: load_skill, description: 加载指定技能的完整指令文档。当技能摘要中的 description 与当前任务匹配时调用。, parameters: { type: object, properties: { name: { type: string, description: 技能名称来自 skills 摘要中的 name 字段 } }, required: [name] } } }工具的执行函数直接调loader.load_skill(name)返回的内容用_strip_frontmatter去掉 YAML 头再包一层skill标签def execute_load_skill(name: str) - str: content loader.load_skill(name) if not content: return fSkill {name} not found. content loader._strip_frontmatter(content) return fskill name{name}\n{content}\n/skill把LOAD_SKILL_TOOL加进tools数组把execute_load_skill注册进执行映射表整条链路就接上了。system prompt 里放summarytools 里放LOAD_SKILL_TOOL模型在需要时会先调load_skill拿到完整文档后再按文档里的步骤调exec或其他工具。这里有个容易踩的坑build_skills_summary()返回的 XML 里description是经过 XML 转义的。如果你自己拼 system prompt别再做一次转义否则amp;会变成amp;amp;模型读起来会困惑。4. 验证请求用 TaoToken 跑通一次完整的技能触发配置写完之后跑一次真实请求来验证。我用的是gpt-4o-mini因为 Function Calling 支持稳定成本也低。请求构造如下import json from openai import OpenAI client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], ) system_prompt f你是一个本地 Agent可以使用以下技能 {summary} 当任务匹配某个技能的 description 时先调用 load_skill 加载完整指令再按指令执行。 messages [ {role: system, content: system_prompt}, {role: user, content: 帮我看看 ./logs/app.log 里有哪些错误}, ] resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages, tools[LOAD_SKILL_TOOL], tool_choiceauto, ) msg resp.choices[0].message print(tool_calls:, msg.tool_calls)预期结果是模型返回一个tool_callsfunction.name是load_skillarguments是{name: log-analyzer}。拿到这个之后执行execute_load_skill把结果作为tool角色消息追加进messages再发一次请求tool_call msg.tool_calls[0] skill_content execute_load_skill(json.loads(tool_call.function.arguments)[name]) messages.append(msg) messages.append({ role: tool, tool_call_id: tool_call.id, content: skill_content, }) resp2 client.chat.completions.create( modelgpt-4o-mini, messagesmessages, tools[LOAD_SKILL_TOOL, EXEC_TOOL], tool_choiceauto, ) print(resp2.choices[0].message)第二次请求里模型应该会调exec去跑grep。如果你看到tool_calls里出现exec并且arguments里的命令是grep -iE error|fail|exception ./logs/app.log说明整条链路通了。实测下来从用户输入到最终执行一共消耗 2 到 3 次模型调用第一次判断要不要加载技能第二次按技能文档执行工具第三次可选整理结果。这就是 Skills 的“渐进式披露”在 Function Calling 层面的真实开销。description写得越准第一次调用的命中率越高整体 Token 消耗越低。提示如果你在第二次请求里发现模型没有调exec而是直接编了一段回答检查skill_content是否真的被放进了tool消息。有些 SDK 对tool消息的content长度有限制超长会被截断。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 报错对照这一节列几个我在调试 Skills 链路时真实遇到的报错以及对应的排查方向。401 Unauthorized最常见。先确认TAOTOKEN_API_KEY环境变量有没有生效echo $TAOTOKEN_API_KEY看输出。如果 Key 没问题检查base_url是不是写成了https://taotoken.net/api/末尾带斜杠改成不带斜杠的https://taotoken.net/api。还有一种情况是 Key 被复制时带了空格strip()一下。local proxy failed / connection refused这个报错通常出现在你本地有代理设置但代理没启动或者端口不对。检查HTTP_PROXY和HTTPS_PROXY环境变量如果不需要代理就unset掉。TaoToken 的 API 通道本身不需要额外代理配置直接请求即可。reading choices of undefined这个报错说明resp.choices是undefined也就是返回体结构不对。大概率是请求打到了错误的 endpoint或者返回了一个错误 JSON 但 SDK 没抛异常。打印完整的resp看看如果是{error: {...}}说明请求被拒了检查 model 名称和 Key 权限。OAuth / authentication_error如果你用的是 Claude Code 或类似工具报 OAuth 相关错误说明工具在走它自己的认证流程而不是用你配的 API Key。这时候需要看工具的配置文件把ANTHROPIC_BASE_URL或对应的 base URL 指向 TaoToken 的地址并且确认 Key 写在了正确的位置。Claude Code 的配置可以参考 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 里的说明。技能不触发不是报错但比报错更常见。三个检查点第一build_skills_summary()的输出有没有真的进 system prompt第二LOAD_SKILL_TOOL有没有加进tools数组第三description是不是写得太被动。Nanobot 的模型天生偏向欠触发description里要明确写出“当用户提到 X 时触发”这样的主动语态。load_skill 返回 not found检查workspace/skills/下的目录名和SKILL.md里的name是否一致。SkillsLoader用目录名作为技能名load_skill(name)也是按目录名找文件。如果目录名是log_analyzer但 frontmatter 里写log-analyzer就会找不到。6. 把 Skills 接进你的 Agent从 SKILL.md 到 Function Calling 的完整闭环回到最开始的问题Nanobot 的 Skills 模块到底在做什么拆完源码和链路之后可以这样理解——SkillsLoader是一个“技能索引器”它把文件系统里的SKILL.md变成一段轻量的 XML 摘要塞进 system prompt。模型看到摘要后通过 Function Calling 调用load_skill把完整文档拉进上下文。然后模型按文档里的步骤继续用 Function Calling 调exec、read_file等工具。整个过程没有新的协议全是 Function Calling 的组合。这意味着两件事。第一Skill 的触发质量取决于description和load_skill工具的 description 是否对齐。第二Skill 的执行质量取决于SKILL.md里的步骤是否足够具体具体到模型能直接翻译成工具调用参数。如果你想在自己的项目里复现这套机制最小实现只需要三个文件一个SKILL.md、一个SkillsLoader的简化版扫描目录 解析 frontmatter 生成摘要、一个load_skill工具注册。模型侧用 TaoToken 的统一通道省去多厂商适配的麻烦。跑通之后你可以逐步加技能每个技能就是一个目录加一个 Markdown 文件不需要改核心代码。最后留一个实用技巧在SKILL.md的步骤里尽量把工具调用的参数写成示例。比如不要只写“用 grep 搜索错误”而是写grep -iE error|fail logfile。模型看到具体命令翻译成exec参数的准确率会高很多。这是我在多个技能上对比之后发现的差异比单纯优化 description 更有效。
阅读完成 · 觉得有帮助?