1. 先把三层概念摆到工程桌面上
LLM、Agent、Skill 这三个词经常被混着用,落到代码里就出问题:有人把一次 chat completion 叫 Agent,有人把写死的函数叫 Skill,结果架构图画得挺漂亮,一跑就发现根本串不起来。我先把边界说清楚,再带你用 TaoToken 的统一 Key 把三层调用链真正跑通。
LLM 是推理与生成层,输入 messages,输出 token,它本身不碰外部世界。Agent 是规划与调度层,它拿着 LLM 做决策,决定下一步调哪个工具、传什么参数、要不要继续循环。Skill 是可复用能力封装层,一个 Skill 就是一段有明确入参出参的原子能力,比如查天气、读文件、发请求。三者关系是:LLM 提供"想",Skill 提供"做",Agent 把"想"和"做"编排成闭环。
适合谁看:正在把 demo 往生产推的开发者、要给自己产品接大模型的后端同学、以及想搞清楚 Agent 框架到底在编排什么的人。下面所有配置和代码都可以直接复制,TaoToken 在这里的角色是统一 Key 和统一 API 通道——三层调用都走同一个 base_url 和同一个 Key,省掉多供应商来回切换的麻烦。
2. TaoToken 前置:一个 Key 打通三层
TaoToken 的定位是统一的大模型 API 接入通道。你注册后在控制台生成一个 API Key,之后 LLM 调用、Agent 里的模型调用、Skill 内部如果需要模型能力,全都复用这一个 Key 和同一个 base_url。这样做的好处很直接:换模型只改 model 字段,不用改鉴权逻辑;三层链路的日志和用量也集中在一处。
需要提前准备的东西:
- 一个 TaoToken 账号,进控制台创建 API Key
- 本地 Python 3.9+ 或 Node 18+ 环境
- 一个能发 HTTP 请求的终端(curl 就够)
关键地址记一下:官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api ,注意 API 地址不带查询参数。生成 Key 的入口在控制台的 API Keys 页面,模型对话调试入口在模型对话页,长期跑编码和 Agent 任务可以看 Coding Plan。
注意:Key 只存在服务端环境变量里,别写进前端代码或提交到仓库。下面配置统一用
TAOTOKEN_API_KEY这个环境变量名。
3. 可复制配置:settings.json 与 config.toml 骨架
先给两份配置文件骨架,一份给 Node/Claude Code 类工具用的settings.json,一份给 Python 项目用的config.toml。两份都指向同一个 TaoToken 通道。
3.1 settings.json 骨架
{ "env": { "TAOTOKEN_API_KEY": "sk-你的Key", "TAOTOKEN_BASE_URL": "https://taotoken.net/api", "TAOTOKEN_MODEL": "claude-sonnet-4-5" }, "agent": { "max_steps": 8, "tool_timeout_ms": 15000, "allow_skills": ["weather_query", "file_read", "http_fetch"] }, "skill": { "registry_path": "./skills", "auto_reload": true } }max_steps控制 Agent 循环上限,防止死循环烧 token;allow_skills是白名单,只有列进去的 Skill 才能被 Agent 调用,这是生产环境必须做的收敛。
3.2 config.toml 骨架
[llm] base_url = "https://taotoken.net/api" api_key = "${TAOTOKEN_API_KEY}" model = "claude-sonnet-4-5" timeout = 60 [agent] max_steps = 8 tool_timeout = 15 system_prompt = "你是一个会调用工具的助手,需要外部信息时必须调用 Skill。" [skill.weather_query] type = "http" endpoint = "https://api.example.com/weather" method = "GET" [skill.file_read] type = "local" root = "./workspace"${TAOTOKEN_API_KEY}是环境变量占位,运行时注入。Skill 分两类:http型走外部接口,local型操作本地文件,Agent 不关心 Skill 内部怎么实现,只看它的名字和参数 schema。
4. 三层调用示例:从单次 LLM 到 Agent 编排再到 Skill 注册
这一节是重点,按 LLM → Skill → Agent 的顺序往上搭,每层都能单独验证。
4.1 第一层:单次 LLM 调用
先用 curl 确认通道通不通:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-5", "messages": [{"role": "user", "content": "用一句话解释什么是 LLM"}] }'返回里choices[0].message.content就是模型输出。这一步只验证"大脑"能说话,它不会去查任何外部数据。Python 版本:
import os, requests resp = requests.post( "https://taotoken.net/api/v1/chat/completions", headers={"Authorization": f"Bearer {os.environ['TAOTOKEN_API_KEY']}"}, json={ "model": "claude-sonnet-4-5", "messages": [{"role": "user", "content": "用一句话解释什么是 LLM"}], }, timeout=60, ) print(resp.json()["choices"][0]["message"]["content"])4.2 第二层:注册一个 Skill
Skill 的本质是"名字 + 参数 schema + 执行函数"。下面注册一个天气查询 Skill:
import requests SKILL_REGISTRY = {} def register_skill(name, schema, fn): SKILL_REGISTRY[name] = {"schema": schema, "fn": fn} def weather_query(city: str) -> dict: r = requests.get( "https://api.example.com/weather", params={"city": city}, timeout=10, ) return r.json() register_skill( name="weather_query", schema={ "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, fn=weather_query, )Skill 自己不会主动跑,它必须被 Agent 调用。schema 的作用是告诉 LLM"这个工具长什么样",LLM 才能正确生成参数。
4.3 第三层:Agent 编排循环
Agent 的核心是一个循环:把用户目标和 Skill 列表一起发给 LLM,LLM 返回要么是最终答案,要么是一个工具调用请求,Agent 执行 Skill 后把结果塞回对话,继续下一轮。
import json, os, requests BASE = "https://taotoken.net/api/v1/chat/completions" HEADERS = {"Authorization": f"Bearer {os.environ['TAOTOKEN_API_KEY']}"} def build_tools(): return [ { "type": "function", "function": { "name": name, "description": f"执行 {name}", "parameters": meta["schema"], }, } for name, meta in SKILL_REGISTRY.items() ] def run_agent(user_input: str, max_steps: int = 8): messages = [{"role": "user", "content": user_input}] for step in range(max_steps): resp = requests.post( BASE, headers=HEADERS, json={ "model": "claude-sonnet-4-5", "messages": messages, "tools": build_tools(), }, timeout=60, ).json() msg = resp["choices"][0]["message"] messages.append(msg) tool_calls = msg.get("tool_calls") if not tool_calls: return msg["content"] for call in tool_calls: fn_name = call["function"]["name"] args = json.loads(call["function"]["arguments"]) result = SKILL_REGISTRY[fn_name]["fn"](**args) messages.append({ "role": "tool", "tool_call_id": call["id"], "content": json.dumps(result, ensure_ascii=False), }) return "达到最大步数,任务未完成" print(run_agent("北京明天天气怎么样?"))这段代码就是三层的完整协作:LLM 负责判断"要不要调工具、调哪个",Skill 负责真正发请求拿数据,Agent 负责把两者串起来并管理循环。MCP 在这里可以理解为 Skill 注册与调用的标准化协议层,让不同来源的工具用统一格式暴露给 Agent。
5. 验证每层是否生效的检查动作
光跑通不够,得知道每层单独坏了怎么定位。下面是我常用的分层检查清单。
| 层级 | 检查动作 | 期望结果 |
|---|---|---|
| LLM | 发一条纯文本 chat 请求 | 返回 content,无 tool_calls |
| Skill | 直接调用weather_query("北京") | 返回结构化 JSON,不经过 LLM |
| Agent | 问一个需要外部数据的问题 | 日志里出现 tool_calls 和 tool 回填 |
| 通道 | 看响应头与用量 | 同一个 Key 覆盖三层调用 |
具体操作:先单独跑 4.1 的 curl,确认 Key 和 base_url 没问题;再单独跑weather_query("北京"),确认 Skill 本身能拿到数据;最后跑run_agent,在循环里打印step和tool_calls,如果 step 一直是 0 且没有 tool_calls,说明 LLM 没识别出该调工具,通常是 tools 描述写得太模糊。
提示:Agent 不调工具,九成是 system prompt 或工具 description 的问题,不是模型问题。把 description 写具体,比如"查询指定城市的实时天气,参数 city 为中文城市名"。
6. 本篇常见错排查
报 401 或鉴权失败:检查TAOTOKEN_API_KEY是否真的注入到运行环境,echo $TAOTOKEN_API_KEY看一眼。配置文件里的${...}占位符不会自动展开,得靠代码读取环境变量。
Agent 死循环:max_steps没设或设太大,加上 Skill 返回错误时 LLM 反复重试。把max_steps压到 8 以内,并在 Skill 里对异常做兜底返回。
Skill 参数对不上:schema 里写了required: ["city"],但 LLM 传了location。要么改 schema 别名,要么在 Skill 入口做参数归一化。
tools 传了但模型不调:确认用的模型支持 function calling,且tools字段格式正确。部分模型对 tools 支持有限,换一个支持工具调用的模型再试。
三层混在一起调试:最常见的坑。一定按 LLM → Skill → Agent 顺序逐层验证,别一上来就跑完整 Agent,出错时根本不知道是哪层。
7. 继续往下走
三层跑通之后,下一步通常是把 Skill 抽成独立服务、给 Agent 加记忆和反思、把配置从本地文件搬到配置中心。如果你要长期跑编码类或 Agent 类任务,可以看 Coding Plan;想先在线试模型效果,去模型对话页直接发消息最快;Key 管理和新建在 API Keys 页面;接入细节和参数说明在接入文档里。统一用 TaoToken 的 Key 和通道,三层调用链的鉴权、用量、模型切换都在一处,维护成本会低很多。