ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LangGraph 7. Skills 1. 技能 Skills(附完整免费源代码):把 SKILL.md 改到 TaoToken 的 Agent Skills 落地大纲

LangGraph 7. Skills 1. 技能 Skills(附完整免费源代码):把 SKILL.md 改到 TaoToken 的 Agent Skills 落地大纲 1. 从 SKILL.md 到可运行技能LangGraph Agent Skills 落地要解决的真问题如果你正在用 LangGraph 搭 Agent大概率遇到过这种局面研究、写作、评审、PDF 处理这些能力全塞在节点函数里每加一种能力就要改图、改状态定义、改边。图越画越肿能力却没法单独测试更别说复用到另一个项目。Agent Skills 想解决的就是这件事——把能力从主流程里拆出来做成可发现、可加载、可替换的单元。Agent Skills 的业界标准形态很朴素一个技能就是一个文件夹里面至少有一个SKILL.md由 YAML frontmatter 加 Markdown 正文组成。frontmatter 里必须有name和description正文写清楚这个技能怎么用。Agent 的运行流程被拆成四步发现只读 frontmatter约 100 tokens/技能、选择按任务匹配、加载只对选中的技能读完整正文、使用把技能说明作为上下文交给 LLM 执行或直接跑技能目录下的脚本。这套流程叫 Progressive disclosure按需加载不浪费上下文。这篇面向的是想用统一 Key/API 通道跑通技能编排的开发者。我会给出skills_library/目录结构、skill_loader.py的注册与调用配置以及把 endpoint 改到 TaoToken 后的连通性验证动作附完整免费源代码。适合谁已经写过 LangGraph 节点、想让能力可插拔的人或者刚接触 Agent Skills、想先跑通一个最小闭环的人。读完你能拿到一个能直接python main.py跑起来的示例技能库放进去就能被自动发现。2. TaoToken 前置统一 Key/API 通道怎么接进技能执行链技能本身不绑定模型但「使用」这一步要调 LLM。示例里use_skill_with_llm()用的是langchain_openai的ChatOpenAI它接受api_key、base_url、model三个参数。把这三个参数指向 TaoToken整条技能执行链就走同一个通道不用为每个技能单独配 Key。TaoToken 在这里的角色是统一入口一个 Key、一个 Base URL模型 ID 按需切换。对 Agent Skills 这种「技能多、调用散」的场景很合适——技能库可以随便加通道不用动。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 这个不加 UTM。你需要准备的东西不多一个可用的 API Key一个想用的模型 ID以及把base_url设成https://taotoken.net/api。Key 的获取在控制台完成路径是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API Keys 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。如果你还没想好模型可以先去模型对话页试一下 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 确认通道通了再写进.env。这里有个容易踩的点ChatOpenAI的base_url要写到/api这一层不要自己拼/v1或别的后缀否则会出现 404 或路径重复。另一个点是api_key不要硬编码在skill_loader.py里统一走config_parser.py从.env读这样技能库换项目时不用改代码。如果你后续要做长期编码或 Agent 编排可以了解下 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。3. 可复制配置SKILL.md 目录结构、skill_loader 与 .env先把目录结构定下来。技能库根目录是demo_codes/skills_library/每个子文件夹是一个技能里面必须有SKILL.md。一个符合规范的技能长这样demo_codes/ ├── skills_library/ │ ├── README.md │ ├── summarize/ │ │ └── SKILL.md │ ├── pdf/ │ │ └── SKILL.md │ └── doc-coauthoring/ │ └── SKILL.md ├── skill_loader.py ├── config_parser.py ├── main.py ├── requirements.txt └── .envSKILL.md的 frontmatter 用 YAML正文用 Markdown。一个最小可用的summarize/SKILL.md--- name: summarize description: 把长文本压缩成要点并保留来源回链适合信息汇总场景。 --- # Summarize 当用户要求总结、摘要、压缩内容时使用本技能。 ## 步骤 1. 通读输入识别主题与关键结论。 2. 输出 3-5 条要点每条不超过 40 字。 3. 若原文有链接或出处保留回链。skill_loader.py里四个函数对应四步流程。发现阶段只解析 frontmatterimport re from pathlib import Path from typing import Dict, List, Optional, Tuple SKILL_FILENAME SKILL.md DEFAULT_SKILLS_LIBRARY Path(__file__).parent / skills_library FRONTMATTER_PATTERN re.compile(r^---\s*\n(.*?)\n---\s*\n, re.DOTALL) def _parse_frontmatter(raw: str) - Dict[str, str]: m FRONTMATTER_PATTERN.match(raw) if not m: return {} meta: Dict[str, str] {} for line in m.group(1).splitlines(): if : in line: k, v line.split(:, 1) meta[k.strip()] v.strip().strip().strip() return meta def discover_skills(skills_root: Optional[Path] None) - List[Dict[str, str]]: root Path(skills_root) if skills_root else DEFAULT_SKILLS_LIBRARY if not root.is_dir(): return [] result: List[Dict[str, str]] [] for path in sorted(root.iterdir()): if not path.is_dir(): continue skill_md path / SKILL_FILENAME if not skill_md.is_file(): continue try: raw skill_md.read_text(encodingutf-8, errorsreplace) except OSError: continue meta _parse_frontmatter(raw) if meta.get(name): result.append(meta) return result选择阶段先用关键词匹配生产环境可以换成 LLM 判断def select_skill_for_task(task: str, discovered: List[Dict[str, str]]) - Optional[str]: task_lower (task or ).strip().lower() if not task_lower or not discovered: return discovered[0][name] if discovered else None keywords [ ([总结, 摘要, summarize, 压缩, 汇总], summarize), ([pdf, 文档], pdf), ([doc, word, docx], docx), ([协作, coauthor, 文档协作], doc-coauthoring), ] for kws, name in keywords: if any(k in task_lower for k in kws): for d in discovered: if d.get(name) name: return name return discovered[0].get(name) if discovered else None加载阶段只对选中的技能读完整正文去掉 frontmatter 后交给 LLMdef load_skill(skill_name: str, skills_root: Optional[Path] None) - Tuple[str, str]: root Path(skills_root) if skills_root else DEFAULT_SKILLS_LIBRARY skill_md root / skill_name / SKILL_FILENAME if not skill_md.is_file(): return , try: full skill_md.read_text(encodingutf-8, errorsreplace) except OSError: return , body full m FRONTMATTER_PATTERN.match(full) if m: body full[m.end():].strip() return full, body使用阶段把技能说明和用户输入一起交给 LLM这里就是接 TaoToken 的地方def use_skill_with_llm( skill_name: str, skill_instructions: str, user_input: str, api_key: Optional[str] None, base_url: Optional[str] None, model: Optional[str] None, ) - str: try: from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_openai import ChatOpenAI except ImportError: return [需要安装 langchain-openai 才能使用 LLM 执行技能] if not api_key or not str(api_key).strip(): try: from config_parser import skills_config api_key skills_config.api_key or base_url base_url if base_url is not None else (skills_config.base_url or ) model model or skills_config.model except Exception: return [请配置 .env 中的 OPENAI_API_KEY] llm ChatOpenAI( model(model or gpt-4o-mini), api_keyapi_key, base_urlbase_url if base_url else None, temperature0.3, ) prompt ChatPromptTemplate.from_messages([ (system, 你正在使用名为「{skill_name}」的 Agent Skill。请严格按照以下技能说明执行用户请求。\n\n技能说明\n{skill_instructions}), (user, {user_input}), ]) chain prompt | llm | StrOutputParser() return (chain.invoke({ skill_name: skill_name, skill_instructions: skill_instructions or (无额外说明), user_input: user_input, }) or ).strip()config_parser.py从.env读配置.env内容如下OPENAI_API_KEY你的_TaoToken_Key BASE_URLhttps://taotoken.net/api MODEL你的模型IDconfig_parser.pyimport os from dataclasses import dataclass from dotenv import load_dotenv load_dotenv() dataclass class SkillsConfig: api_key: str os.getenv(OPENAI_API_KEY, ) base_url: str os.getenv(BASE_URL, https://taotoken.net/api) model: str os.getenv(MODEL, gpt-4o-mini) skills_config SkillsConfig()requirements.txt写这几行langchain-openai langchain-core python-dotenvmain.py把四步串起来from skill_loader import discover_skills, select_skill_for_task, load_skill, use_skill_with_llm def run(task: str) - None: discovered discover_skills() if not discovered: raise SystemExit(未发现任何技能请按 skills_library/README.md 放置技能。) print(已发现技能仅元数据) for d in discovered: desc (d.get(description) or )[:60] print(f - {d.get(name, )}: {desc}) skill_name select_skill_for_task(task, discovered) or discovered[0][name] print(选中技能, skill_name) _, body load_skill(skill_name) if not body: raise SystemExit(f无法加载技能 {skill_name} 的 SKILL.md) print(f已加载技能正文约 {len(body)} 字符) result use_skill_with_llm(skill_name, body, task) print(结果, result) if __name__ __main__: import sys run(sys.argv[1] if len(sys.argv) 1 else 请总结并保留要点LangGraph 是用于构建多步推理与智能体工作流的库支持状态图与条件边。)4. 验证请求跑通发现→选择→加载→使用并确认通道连通配置齐了先装依赖再跑。进入demo_codes目录pip install -r requirements.txt python main.py预期输出分四段。第一段是发现结果只打印元数据不读正文已发现技能仅元数据 - doc-coauthoring: Guide users through a structured workflow for co-authoring d... - pdf: Use this skill whenever the user wants to do anything with P... - summarize: 把长文本压缩成要点并保留来源回链适合信息汇总场景。第二段是选择结果任务里带「总结」关键词应该命中summarize选中技能 summarize第三段是加载结果只对选中的技能读完整正文已加载技能正文约 320 字符第四段是使用结果这一步真正打到 TaoToken 的/api端点。如果通道通了会返回模型生成的要点文本如果 Key 或 Base URL 有问题这里会报错下一节专门排。想验证「选择」逻辑是否按任务走可以换一个任务再跑python main.py 用三句话概括 PDF 文档处理常见需求。这次任务里带「PDF」应该命中pdf技能加载的正文长度也会变。这一步能确认发现和选择是解耦的——发现阶段不读正文选择阶段只比对元数据加载阶段才读正文。你可以故意把某个技能的description改掉观察选择结果是否变化以此确认匹配逻辑生效。如果要在 LangGraph 图里用把「使用技能」做成一个节点即可节点内部依次调discover_skills、select_skill_for_task、load_skill、use_skill_with_llm状态里存task和result。这样技能库增删不影响图结构只影响节点内部的发现结果。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth跑不通时先看报错落在哪一步。发现和加载阶段不碰网络报错基本是路径问题使用阶段才碰网络报错集中在 Key、Base URL、模型 ID。401 Unauthorizedapi_key没读到或值不对。先确认.env里OPENAI_API_KEY有值且没有多余空格再确认config_parser.py的load_dotenv()在读取前执行。如果.env放在demo_codes目录而你在别的目录跑python main.pyload_dotenv()可能找不到文件用绝对路径或先cd到demo_codes。还有一种情况是 Key 复制时带了换行strip 一下。local proxy failed / connection errorBASE_URL写错或网络不通。确认值是https://taotoken.net/api不要写成https://taotoken.net/api/v1或漏掉https。如果本机有环境变量HTTP_PROXY、HTTPS_PROXY指向了不可用的地址langchain_openai会走这个代理导致连接失败临时清掉再试。注意这里说的是本机环境变量不是让你去配什么网络工具。reading choices / KeyError choices响应结构不是预期的 OpenAI 格式通常是base_url指到了错误路径或者model填了一个该通道不支持的 ID。先确认base_url是https://taotoken.net/api再确认MODEL是通道里可用的模型 ID。可以先用模型对话页发一条消息确认这个模型 ID 能出结果再写回.env。OAuth / authentication 相关报错如果你用的是需要 OAuth 的客户端比如某些 CLI 工具它的凭据文件和.env是两套东西。本篇示例走的是 API Key不涉及 OAuth。如果你在别的工具里遇到 OAuth 报错检查那个工具自己的凭据配置不要和这里的.env混用。技能没被发现discover_skills返回空列表。检查skills_library/下每个技能文件夹里是否有SKILL.md文件名大小写是否一致frontmatter 是否有name字段。_parse_frontmatter只认---包裹的 YAML如果SKILL.md开头没有 frontmatter这个技能会被跳过。加载正文为空load_skill返回空字符串。检查技能名和文件夹名是否一致select_skill_for_task返回的名字要能在skills_library/下找到同名文件夹。如果 frontmatter 解析后正文被切没了检查---是否成对出现。6. 把技能接进你的项目从示例到可复用技能库示例跑通后最有价值的动作是把你项目里重复出现的能力抽成技能。判断标准很简单如果一段逻辑在多个图或产品里都要用且它有自己的输入输出说明就适合做成SKILL.md。抽的时候只写「怎么用」不写「怎么实现」——实现可以放在技能目录的scripts/下正文里引用脚本路径即可。技能库可以独立成仓库多个项目共用。skill_loader.py里的DEFAULT_SKILLS_LIBRARY改成环境变量或参数传入就能指向不同技能库。选择逻辑从关键词匹配升级到 LLM 判断时把候选技能的name和description拼成 prompt 让模型选返回技能名其余流程不变。如果你要把这套接进 Claude Code 或类似工具配置三件套是 Base URL、Key、Model IDBase URL 用https://taotoken.net/apiKey 用控制台生成的Model ID 按需选。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API Keys 在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。长期做编码或 Agent 编排的话Coding Plan 在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。最后留一个实操建议先别急着加很多技能放两个进去把发现、选择、加载、使用四步的输出都打印出来看一遍。确认每一步的输入输出符合预期再加第三个。技能库的扩展成本很低但流程没跑通时加技能只会让排查变难。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进