ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

langchain deepagents 框架使用带脚本的 Skill:从 settings.json 到可复现验证

langchain deepagents 框架使用带脚本的 Skill:从 settings.json 到可复现验证 1. 为什么要在 deepagents 里用带脚本的 Skilllangchain deepagents 框架里的 Skill本质上是给 Agent 装一个「可插拔的能力包」。纯文本 Skill 只能让模型按说明去推理而带脚本的 Skill 会把一段真实可执行的 Python 逻辑挂到 Agent 上让它能查数据库、调接口、跑计算而不是靠模型「编」。我这次要落地的场景很具体在本地项目里通过settings.json骨架声明一个带脚本的 Skill 入口让 deepagents 在运行时能发现它、读取它的SKILL.md、并通过脚本执行器真正跑起来。同时把模型调用通道统一到 TaoToken 的 Key/API 上保证整条链路可复现。适合谁看已经在用 langchain 或 deepagents 写过 Agent、但卡在「Skill 脚本怎么被 Agent 真正调用」这一步的同学以及想把脚本能力标准化、不想每次改代码的人。读完你能拿到一份可复制的settings.json片段、一个最小可跑的 Skill 目录结构以及一套逐步验证动作用来判断脚本 Skill 到底有没有按预期执行。核心检索词先摆出来langchain、deepagents、Skill、脚本、框架。这四个词贯穿全文后面所有配置和排障都围绕它们展开。2. TaoToken 前置统一 Key 与 API 通道在写 Skill 之前先把模型通道固定下来。deepagents 底层还是走 LangChain 的模型接口所以只要把 base_url 和 api_key 指向 TaoToken就能让整个 Agent 的推理请求走同一条通道后面 Skill 脚本里如果也要调模型复用同一套环境变量即可。TaoToken 在这里的角色是「统一 Key/API 通道」一个 Key 覆盖多种模型省得在settings.json、.env、脚本里各写一份。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 这个不加 UTM。操作上分三步第一步去控制台建 Key。打开 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面新建一个复制出来。这个页面地址是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 两个入口都能到。第二步把 Key 写进环境变量不要硬编码进settings.json。我习惯在项目根目录建.env# .env TAOTOKEN_API_KEYsk-你的key TAOTOKEN_BASE_URLhttps://taotoken.net/api第三步在代码里读取。LangChain 的 ChatOpenAI 兼容接口可以直接吃这两个值import os from dotenv import load_dotenv from langchain_openai import ChatOpenAI load_dotenv() llm ChatOpenAI( modelclaude-sonnet-4-5, api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], temperature0, )注意base_url结尾不要多加/v1TaoToken 的兼容层会自己处理路径。多写一层常见报 404。如果你只是想先确认模型通不通可以直接用模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 发一句话能回就说明 Key 没问题再往下写 Skill。3. 可复制配置settings.json 骨架与 Skill 目录deepagents 的 Skill 发现机制是「扫目录 读 frontmatter」。所以配置分两块一块是settings.json声明 skills 根目录和脚本执行器一块是 Skill 自己的目录结构。先看目录结构这是最小可跑单元my-agent/ ├── settings.json ├── .env └── skills/ └── arxiv-search/ ├── SKILL.md └── arxiv_search.pysettings.json骨架长这样直接复制改路径即可{ agent: { name: research-agent, model: claude-sonnet-4-5, base_url: https://taotoken.net/api }, skills: { enabled: true, skills_dir: ./skills, auto_load: true, script_executor: { enabled: true, runtime: python, timeout_seconds: 30, allow_subprocess: true } }, tools: { execute_python_script: { enabled: true, max_output_chars: 8000 } } }几个字段值得单独说。skills_dir是扫描根目录deepagents 会遍历它下面每个子目录找SKILL.md。script_executor.runtime指定用哪个解释器写python就走当前虚拟环境。timeout_seconds是子进程硬超时脚本卡死时靠它兜底。allow_subprocess必须为 true否则脚本执行器不会真的起进程。然后是SKILL.md格式必须是 YAML frontmatter不能用 Markdown 表格--- name: arxiv-search description: Searches arXiv for preprints by keyword and returns titles plus links. license: MIT --- Run the bundled Python script: .venv/bin/python [YOUR_SKILLS_DIR]/arxiv-search/arxiv_search.py query --max-papers Nname和description是必填Agent 靠description判断什么时候该用这个 Skill。正文里写清楚调用命令模型会照着拼参数。脚本本身保持「纯函数」风格输入输出都走命令行# skills/arxiv-search/arxiv_search.py import argparse import urllib.parse import urllib.request import xml.etree.ElementTree as ET def search(query: str, max_papers: int) - str: base http://export.arxiv.org/api/query? params urllib.parse.urlencode({ search_query: fall:{query}, start: 0, max_results: max_papers, }) with urllib.request.urlopen(base params, timeout20) as resp: raw resp.read().decode(utf-8) root ET.fromstring(raw) ns {a: http://www.w3.org/2005/Atom} lines [] for entry in root.findall(a:entry, ns): title entry.find(a:title, ns).text.strip().replace(\n, ) link entry.find(a:id, ns).text.strip() lines.append(fTitle: {title}\nLink: {link}) return \n\n.join(lines) if lines else No results. if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(query) parser.add_argument(--max-papers, typeint, default5) args parser.parse_args() print(search(args.query, args.max_papers))脚本只依赖标准库避免环境问题。输出用print执行器会捕获 stdout 回传给 Agent。4. 验证请求从加载到脚本执行配置写完接下来是逐步验证。不要一上来就跑完整 Agent分四步走每步都能单独判断。第一步验证 Skill 被发现。写个小脚本调 SkillsMiddleware 的加载逻辑from deepagents.middleware.skills import SkillsMiddleware mw SkillsMiddleware(skills_dir./skills) loaded mw.load_skills() for s in loaded: print(s.name, -, s.path)期望输出arxiv-search - ./skills/arxiv-search/SKILL.md。如果这里是空的说明目录层级或 frontmatter 有问题先别往下走。第二步验证脚本能独立跑通。直接命令行执行.venv/bin/python skills/arxiv-search/arxiv_search.py deep learning games --max-papers 3期望看到 3 条Title:开头的记录。这一步失败就是脚本本身的问题跟 Agent 无关。第三步验证执行器工具。单独调execute_python_scriptfrom langchain_core.tools import tool import subprocess, sys, shlex tool def execute_python_script(command: str) - str: Execute a python script command and return stdout. parts shlex.split(command) result subprocess.run( [sys.executable] parts, capture_outputTrue, textTrue, timeout30, ) if result.returncode ! 0: return fERROR: {result.stderr} return result.stdout print(execute_python_script.invoke( skills/arxiv-search/arxiv_search.py deep learning games --max-papers 3 ))注意这里用的是tool装饰器不是 BaseTool 子类。原因在下一节排障里细说。第四步跑完整 Agent。把 Skill 描述注入系统提示让模型自己决定读SKILL.md再调工具from langchain.agents import create_react_agent, AgentExecutor from langchain_core.prompts import PromptTemplate prompt PromptTemplate.from_template( You have skills available:\n{skills}\n\n Use execute_python_script to run skill scripts.\n\n Question: {input}\n{agent_scratchpad} ) agent create_react_agent(llm, [execute_python_script], prompt) executor AgentExecutor(agentagent, tools[execute_python_script], verboseTrue) result executor.invoke({ input: 帮我在 arxiv 搜索深度学习在游戏领域的论文返回5篇, skills: - arxiv-search: Searches arXiv for preprints by keyword., }) print(result[output])成功时你会看到消息轮次HumanMessage 提问 → AIMessage 决定读 SKILL.md → ToolMessage 返回说明 → AIMessage 拼命令 → ToolMessage 返回论文列表 → AIMessage 整理输出。如果模型第一次查询词不够精确它会自己换词重试这是带脚本 Skill 比纯文本 Skill 强的地方——它能拿到真实结果再判断。5. 本篇常见错排查问题一Skill 没被加载Agent 感知不到。九成是SKILL.md用了 Markdown 表格而不是 YAML frontmatter。deepagents 的解析器只认---包裹的 YAML 块。检查文件头三行是不是---、name:、description:。问题二PydanticJsonSchemaWarning: Default value is not JSON serializable。这是用 BaseTool 子类 复杂 Pydantic 模型导致的。一个只吃路径字符串的简单工具没必要上ExecutePythonInput(BaseModel)。直接用tool装饰器参数就是普通函数签名LangChain 自动推断 schema。问题三LLM 反复传{args_schema: ...}当参数。这是 BaseTool 的保留字段被暴露了。当你继承 BaseTool 又没显式声明args_schema时LangChain 会把整个类属性包括args_schema、description都塞进 tool schema模型看到这些字段名就误以为是输入参数。彻底解法还是放弃 BaseTool改用tool模型只会看到command一个参数。问题四LLM 用description当参数名。同源问题BaseTool 自动推断 schema 时把父类的description: str也暴露了。tool装饰器只暴露函数签名里的参数从根上规避。问题五脚本执行超时或返回空。先确认settings.json里timeout_seconds够用arXiv 查询偶尔慢。再确认脚本用的是绝对路径或相对项目根的路径子进程的工作目录可能和你想的不一样。最后看max_output_chars输出被截断时 Agent 会以为没结果。问题六base_url报 404。检查是不是写成了https://taotoken.net/api/v1。TaoToken 兼容层自己处理版本路径多写一层就找不到。正确值是https://taotoken.net/api。6. 把通道固定下来再谈 Skill 扩展脚本 Skill 跑通之后真正省事的地方在于新增能力只需要加一个目录、一个SKILL.md、一个脚本settings.json不用动。Agent 会在启动时自动扫到模型会根据description决定用不用。如果你打算长期跑编码类或 Agent 类任务建议把 Key 和额度单独规划Coding Plan 入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 适合高频调用场景。接入细节和参数说明看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面把兼容层的行为写得比较清楚。Claude Code 相关的接入说明在 https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 需要的话可以对照。最后留一个我踩过的坑脚本里不要print调试信息混在结果里执行器会把 stdout 整体回传模型分不清哪行是数据哪行是日志。要调试就写 stderr执行器只在 returncode 非零时才把 stderr 当错误返回。
RELATED READING

延伸阅读

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