1. 多模型接入的真实痛点为什么你的 Agent 代码越写越乱如果你正在做 AI Agent 或者智能硬件里的对话模块大概率遇到过这个场景项目一开始只接了 OpenAI代码干净利落后来产品说 Claude 效果好加一套 Anthropic 的 SDK再后来成本考虑要接 DeepSeek又加一套测试同学说 Gemini 便宜想对比一下再来一套。三个月后你打开providers/目录发现里面躺着五六个文件每个文件里都在重复处理messages格式、tools参数、temperature范围、错误码映射。这就是模型接口封装要解决的核心问题。不同厂商的 API 看起来都是「发消息、收回复」但细节差异非常多OpenAI 的tool_calls是数组嵌套在choices[0].message里Anthropic 的tool_use是 content block 的一种类型Gemini 又用functionCall字段。参数名也不统一有的叫max_tokens有的叫max_output_tokens。认证方式更是五花八门Bearer Token、x-api-key、query 参数都有。这篇要讲的是怎么用 TaoToken 的统一 Key 和 API 通道配合 LiteLLMProvider 做接口封装再用 ContextBuilder 做 Prompt 组装把「多模型接入」这件事从「每个模型写一套适配」变成「配置驱动 统一调用」。适合正在做 Agent 框架、智能硬件对话模块、或者多模型对比评测的开发者。读完之后你能拿到一份可复制的config.toml和settings.json跑通一次从 Prompt 组装到模型响应的完整链路。2. TaoToken 前置准备统一 Key 与 API 通道TaoToken 在这里扮演的角色是「统一入口」。你不需要为每个模型厂商单独申请 Key、单独配置 base_url、单独处理认证头。TaoToken 提供一个统一的 API 地址和一把 Key后面接的是哪家模型由请求里的model字段决定。先做三件事。第一拿到 API Key。访问控制台页面创建https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite创建后复制那串sk-开头的 Key后面配置里要用。第二确认 API 基础地址。TaoToken 的 API 端点是https://taotoken.net/api注意这个地址不加 UTM 参数直接作为base_url使用。OpenAI 兼容模式下实际请求路径是https://taotoken.net/api/v1/chat/completions。第三想清楚你要接哪些模型。TaoToken 的模型命名遵循provider/model-name的格式比如openai/gpt-4o、anthropic/claude-3-5-sonnet、deepseek/deepseek-chat。这个命名规则很重要因为 LiteLLMProvider 就是靠前缀来判断该走哪条路由、该加哪些特定参数。注意不要把 Key 硬编码在代码里提交到 Git。用环境变量或者本地配置文件后面config.toml里会演示怎么引用。如果你还没决定用哪些模型建议先用一个便宜快速的模型跑通链路比如deepseek/deepseek-chat或者openai/gpt-4o-mini验证成功后再换成主力模型。3. 可复制配置config.toml 与 settings.json这一节给两份配置。config.toml是 LiteLLMProvider 的运行时配置settings.json是 ContextBuilder 的 Prompt 组装配置。两份配合使用。3.1 config.tomlLiteLLMProvider 配置骨架# config.toml [llm] provider litellm api_base https://taotoken.net/api api_key_env TAOTOKEN_API_KEY # 从环境变量读取不写死 default_model deepseek/deepseek-chat fallback_model openai/gpt-4o-mini timeout 60 max_retries 2 [llm.params] temperature 0.7 max_tokens 4096 top_p 0.95 [llm.budget] context_window 128000 reserve_ratio 0.1 # 留 10% 缓冲 min_response_tokens 256 [llm.provider_overrides] # 按模型前缀覆盖参数解决不同厂商参数不兼容 gemini/ { reasoning_effort medium } openai/ { top_k unsupported } anthropic/ { top_k 40 }几个关键点解释一下。api_base指向 TaoToken 的 API 地址api_key_env指定从哪个环境变量读 Key这样配置文件可以安全地进版本库。provider_overrides是解决参数兼容问题的核心Gemini 支持reasoning_effort但不支持top_kOpenAI 不支持top_k和min_pAnthropic 支持top_k。用前缀匹配的方式做条件注入避免「一个参数发给所有模型」导致 400 错误。设置环境变量export TAOTOKEN_API_KEYsk-你的KeyWindows PowerShell$env:TAOTOKEN_API_KEYsk-你的Key3.2 settings.jsonContextBuilder 配置骨架{ context: { system_prompt_file: prompts/AGENTS.md, user_prefs_file: prompts/USER.md, memory_file: MEMORY.md, memory_dir: memory, skills_dir: skills, max_history_messages: 20, always_include_skills: [core], inject_env_info: true, warn_thresholds: { tokens_low: 3000, tokens_critical: 1500, tool_calls_low: 5, tool_calls_critical: 1 } }, session: { store: file, path: sessions, ttl_hours: 72 } }system_prompt_file指向人格设定文件memory_file是长期记忆max_history_messages控制短期记忆的滑动窗口大小。warn_thresholds是动态警告的触发阈值当剩余 token 或工具调用次数低于这些值时ContextBuilder 会在工具结果里注入提醒引导模型优先解决关键问题。3.3 目录结构配置引用的文件需要存在建议这样组织project/ ├── config.toml ├── settings.json ├── prompts/ │ ├── AGENTS.md # 人格设定 │ └── USER.md # 用户偏好 ├── MEMORY.md # 长期记忆 ├── memory/ # 每日笔记 ├── skills/ # 技能文档 └── sessions/ # 会话历史AGENTS.md写人格设定比如「你是一个专注于代码审查的助手回答简洁优先指出潜在 bug」。USER.md写用户偏好比如「用户是 Python 开发者偏好类型注解不喜欢过度解释」。这两个文件的内容会被 ContextBuilder 拼进系统提示。4. 调用链路与验证从 Prompt 组装到模型响应配置就绪后跑一次完整请求验证链路。下面用 Python 演示核心是 LiteLLMProvider 的complete()方法和 ContextBuilder 的build()方法。4.1 LiteLLMProvider 封装# providers/litellm_provider.py import os import importlib import tomllib from typing import List, Dict, Optional class LiteLLMProvider: def __init__(self, config_path: str config.toml): with open(config_path, rb) as f: cfg tomllib.load(f) self.cfg cfg[llm] self._litellm None # 懒加载 def _get_litellm(self): if self._litellm is None: self._litellm importlib.import_module(litellm) return self._litellm def _build_kwargs(self, messages: List[Dict], tools: Optional[List] None): params dict(self.cfg.get(params, {})) kwargs { model: self.cfg[default_model], messages: messages, api_base: self.cfg[api_base], api_key: os.environ[self.cfg[api_key_env]], timeout: self.cfg.get(timeout, 60), **params, } if tools: kwargs[tools] tools kwargs[tool_choice] auto # 按前缀注入 provider 特定参数 overrides self.cfg.get(provider_overrides, {}) for prefix, extra in overrides.items(): if kwargs[model].startswith(prefix): for k, v in extra.items(): if v ! unsupported: kwargs[k] v return kwargs async def complete(self, messages, toolsNone): litellm self._get_litellm() kwargs self._build_kwargs(messages, tools) try: resp await litellm.acompletion(**kwargs) return self._normalize(resp) except Exception as e: # 主模型失败时切 fallback if self.cfg.get(fallback_model): kwargs[model] self.cfg[fallback_model] resp await litellm.acompletion(**kwargs) return self._normalize(resp) raise def _normalize(self, resp): choice resp.choices[0] return { content: choice.message.content, tool_calls: getattr(choice.message, tool_calls, None), usage: resp.usage.model_dump() if resp.usage else {}, }懒加载那段很关键。LiteLLM 导入耗时大概 1 到 2 秒如果放在模块顶层每次启动 CLI 都要等。用importlib推迟到第一次调用时再导入启动速度明显改善。4.2 ContextBuilder 组装# agent/context.py import json from pathlib import Path from typing import List, Dict class ContextBuilder: def __init__(self, settings_path: str settings.json): self.cfg json.loads(Path(settings_path).read_text())[context] def _read(self, path: str) - str: p Path(path) return p.read_text(encodingutf-8) if p.exists() else def _build_system_prompt(self) - str: parts [] identity self._read(self.cfg[system_prompt_file]) parts.append(identity or 你是一个有用的 AI 助手。) if self.cfg.get(inject_env_info): from datetime import datetime parts.append(f当前时间{datetime.now().isoformat()}) parts.append(f工作目录{Path.cwd()}) prefs self._read(self.cfg[user_prefs_file]) if prefs: parts.append(f用户偏好{prefs}) return \n\n.join(parts) def build(self, user_message: str, history: List[Dict] None) - List[Dict]: messages [] messages.append({role: system, content: self._build_system_prompt()}) memory self._read(self.cfg[memory_file]) if memory: messages.append({role: system, content: f【长期记忆】\n{memory}}) if history: messages.extend(history[-self.cfg[max_history_messages]:]) messages.append({role: user, content: user_message}) return messages4.3 跑通一次对话# main.py import asyncio from providers.litellm_provider import LiteLLMProvider from agent.context import ContextBuilder async def main(): provider LiteLLMProvider(config.toml) builder ContextBuilder(settings.json) messages builder.build(用一句话解释什么是 Prompt 工程) print( 组装后的 messages ) for m in messages: print(f[{m[role]}] {m[content][:80]}...) result await provider.complete(messages) print(\n 模型响应 ) print(result[content]) print(\n Token 用量 ) print(result[usage]) asyncio.run(main())预期输出先打印组装后的 messages 数组能看到 system 角色的人格设定、长期记忆、用户消息依次排列然后打印模型回复最后打印 token 用量包含prompt_tokens、completion_tokens、total_tokens。如果这一步成功返回了内容说明从 TaoToken 统一 Key 到 LiteLLMProvider 封装、再到 ContextBuilder 组装的整条链路已经跑通。接下来换default_model为anthropic/claude-3-5-sonnet或者openai/gpt-4o不需要改任何代码只改配置就能切换模型。5. 本篇常见错排查5.1 401 Unauthorized最常见的原因是环境变量没生效。检查echo $TAOTOKEN_API_KEY是否有输出。另一个原因是config.toml里api_key_env写的变量名和实际设置的不一致。还有一种情况是 Key 复制时带了空格或换行用strip()处理一下。5.2 404 Not Foundapi_base配置错误。TaoToken 的 API 地址是https://taotoken.net/apiLiteLLM 会自动拼接/v1/chat/completions。如果你手动在api_base里加了/v1就会变成/v1/v1/chat/completions导致 404。去掉多余的路径段。5.3 400 Bad Request参数不兼容典型报错是top_k is not supported for this model或者reasoning_effort is not a valid parameter。这就是provider_overrides要解决的问题。检查你的模型前缀是否匹配到了对应的 override 规则。比如openai/gpt-4o会匹配openai/前缀把top_k标记为unsupported从而跳过注入。5.4 上下文超限context_length_exceeded说明max_history_messages设太大了或者MEMORY.md内容过长。先调小max_history_messages到 10 试试。如果长期记忆文件超过几千字考虑做摘要压缩只保留关键事实。budget.context_window要和实际模型的窗口大小一致比如deepseek/deepseek-chat是 64Kopenai/gpt-4o是 128K写错了会导致预算计算偏差。5.5 模型响应为空但没报错检查max_tokens是否设得太小。有些模型在max_tokens小于 16 时会直接返回空。另外检查temperature是否设成了 0 且 prompt 过于模糊某些模型在这种情况下会返回空字符串。把temperature调到 0.7 再试。5.6 切换模型后行为差异大这是正常的不同模型的指令遵循能力不同。如果系统提示里写了「回答简洁」GPT-4o 可能严格遵守但某些小模型会忽略。解决办法是在AGENTS.md里把要求写得更具体比如「回答不超过三句话不使用列表格式」。Prompt 工程本身就是针对不同模型做微调的过程。6. 下一步把统一 Key 用在长期编码与 Agent 场景链路跑通之后你手里就有了一套「配置驱动、统一调用」的模型接入层。接下来可以往两个方向走。一个方向是把它接到 Coding Plan 里让 Agent 在写代码、改 bug、跑测试的循环中自动调用模型。TaoToken 的 Coding Plan 页面有具体的接入方式https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite另一个方向是继续打磨 Prompt 工程。ContextBuilder 目前只做了系统提示、长期记忆、历史消息的拼接你可以在此基础上加技能摘要、动态警告、多轮工具调用结果回填。这些在后续文章里会展开。如果你还没创建 API Key从这里进https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite接入文档在这里遇到参数问题可以对照查https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite想先在网页上试试模型对话效果不用写代码https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite我自己的习惯是新模型先在上面这个对话页面手动试几轮确认它的指令遵循风格和输出格式符合预期再写进config.toml的default_model。这样能避免「配置改了半天跑起来发现模型根本不按套路出牌」的尴尬。