1. 从“只会聊天”到“能干活”AI开发全家桶到底解决什么问题很多人第一次用大模型都会经历一个相似的落差问它常识、写文案、改代码感觉像个天才可一旦让它查一下公司内部文档、调一次天气接口、跑一遍数据库查询它立刻开始一本正经地胡说八道。这不是模型变笨了而是它的本质决定的——大语言模型是一个基于海量语料训练出来的“下一个词元概率预测器”它的知识停在训练截止日它没有手也没有脚无法和真实世界交互。于是就有了我们今天要串起来的这套“全家桶”Prompt 负责把话说清楚RAG 负责给它外挂一个动态知识库Function Calling 负责让它能真正调用外部函数MCP 负责把工具接入这件事标准化Agent 则站在最上层用大模型的推理能力去规划、分解并指挥这一切。你可以把它想象成一个超级助理LLM 是大脑Prompt 是工作指令RAG 是随身资料库Function Calling 是手脚MCP 是连接大脑和手脚的神经系统Agent 是那个会自己排计划的总指挥。这套东西听起来概念很多但真正落地时最容易被卡住的其实不是算法而是“通道”——你得有一个稳定、统一、能同时跑通对话、嵌入、函数调用和工具协议的 API 入口。我这次全程用 TaoToken 作为统一 Key 和 API 通道来串联好处是 Base URL 和 Key 只配一次后面 RAG、Function Calling、MCP、Agent 全部复用不用在四五个平台之间来回切换。下面我会按“先配通道再逐模块跑通最后组装 Agent”的顺序把每一步的可复制配置和验证动作都写清楚你跟着做就能跑起来。2. 前置准备用 TaoToken 统一 Key 打通全链路 API 通道在写任何业务代码之前先把 API 通道配好这是后面所有模块的地基。TaoToken 的官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 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。第二步确认你要用的模型 ID。不同任务对模型的要求不一样Prompt 调试和日常对话可以用通用对话模型RAG 里的向量化需要嵌入模型Function Calling 和 Agent 规划建议用推理能力更强的模型。模型清单可以在模型对话页 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 里查看选好后把 Model ID 记下来后面配置里会反复用到。第三步把通道写进环境变量。我习惯用.env文件管理这样 RAG、Function Calling、MCP 三套代码可以共用同一份配置# .env TAOTOKEN_API_KEYsk-你的密钥 TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_MODELgpt-4o-mini TAOTOKEN_EMBED_MODELtext-embedding-3-small这里有个关键点Base URL 一定要写成https://taotoken.net/api很多 OpenAI SDK 默认会拼/v1/chat/completions如果你的客户端库版本较新直接用这个 Base URL 就能正确路由。如果你用的是需要显式写/v1的老版本 SDK就写成https://taotoken.net/api/v1两种写法取决于你的库版本实测下来新版 openai Python SDK 用不带/v1的写法更省心。第四步做一次最小连通性验证。先别急着写 RAG先用一段最简单的对话请求确认 Key 和 Base URL 是通的import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client OpenAI( api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL), ) resp client.chat.completions.create( modelos.getenv(TAOTOKEN_MODEL), messages[{role: user, content: 用一句话说明什么是RAG}], ) print(resp.choices[0].message.content)如果这段能正常打印出内容说明通道已经打通后面所有模块都可以复用这个 client。如果报 401先检查 Key 有没有复制完整、有没有多余空格如果报连接错误检查 Base URL 是不是写成了带 UTM 的地址——API 地址不要带任何查询参数。这一步看起来简单但它是后面所有环节的前提。我见过太多人 RAG 跑不通、Function Calling 报错最后发现是 Base URL 写错或者 Key 用错了项目。所以先把这一层锁死再往上叠功能。3. 可复制配置Prompt、RAG、Function Calling 三件套落地通道通了之后我们按 Prompt → RAG → Function Calling 的顺序逐个跑通。这三块是 Agent 的三大件先分别验证再组装。3.1 Prompt 工程用结构化模板把模型框住Prompt 不是玄学它的核心就是给模型画一个圈。模型训练数据太杂回答边界模糊Prompt 的作用就是告诉它“你是谁、做什么、按什么规则、参考什么信息、输出什么格式”。我常用的公式是角色 任务 背景 要求 格式/范例。下面是一个可以直接复用的结构化 Prompt 模板我把它写成一个 Python 函数方便后面在 RAG 和 Agent 里调用PROMPT_TEMPLATE 你是一位{role}。 你的任务是{task} 背景信息{context} 约束要求 1. 只基于背景信息回答不要编造背景中没有的内容 2. 如果背景信息不足以回答直接说“根据现有资料无法回答” 3. 回答要分点每点不超过两句话 输出格式{format} def build_prompt(role, task, context, fmt纯文本): return PROMPT_TEMPLATE.format( rolerole, tasktask, contextcontext, fmtfmt )这个模板的关键在于第 2 条约束——它直接对应 RAG 场景里最头疼的“幻觉”问题。当你把检索到的文档片段塞进context时模型被明确要求“只基于背景信息回答”这样即使检索结果不完整它也会老实说“无法回答”而不是自己编一个。你可以先用这个模板做一次不带 RAG 的测试把context留空看模型会不会遵守“无法回答”的约束。如果它还是硬答说明约束写得不够强可以把第 2 条改成“如果背景信息为空或与问题无关必须回答‘根据现有资料无法回答’不得使用你自己的知识”。3.2 RAG给模型外挂一个动态知识库RAG 的流程是五步用户提问 → 问题向量化 → 向量库检索 → 检索片段填入 Prompt → 模型生成回答。我们用一个最小可运行的本地 RAG 来演示不依赖外部向量数据库用 numpy 做余弦相似度就够了。先装依赖pip install openai numpy python-dotenv然后写一个完整的 RAG 脚本import os import numpy as np from openai import OpenAI from dotenv import load_dotenv load_dotenv() client OpenAI( api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL), ) # 1. 准备知识库实际项目中替换为你的文档切片 knowledge_base [ TaoToken 的 API 地址是 https://taotoken.net/api控制台在 /console 路径下。, 创建 API Key 需要登录后进入 API Keys 页面新建后复制保存。, 模型对话页面可以查看当前可用的模型列表和 Model ID。, Coding Plan 适合长期编码和 Agent 场景按周期计费更划算。, ] def get_embedding(text): resp client.embeddings.create( modelos.getenv(TAOTOKEN_EMBED_MODEL), inputtext, ) return resp.data[0].embedding # 2. 预先向量化知识库 kb_vectors np.array([get_embedding(t) for t in knowledge_base]) def retrieve(query, top_k2): q_vec np.array(get_embedding(query)) # 余弦相似度 sims kb_vectors q_vec / ( np.linalg.norm(kb_vectors, axis1) * np.linalg.norm(q_vec) ) idx np.argsort(sims)[::-1][:top_k] return [knowledge_base[i] for i in idx] def rag_answer(query): contexts retrieve(query) context_text \n.join(f- {c} for c in contexts) prompt build_prompt( roleTaoToken 使用助手, taskquery, contextcontext_text, ) resp client.chat.completions.create( modelos.getenv(TAOTOKEN_MODEL), messages[{role: user, content: prompt}], ) return resp.choices[0].message.content, contexts if __name__ __main__: answer, ctx rag_answer(TaoToken 的 API 地址是什么) print(检索到的片段, ctx) print(回答, answer)跑通这个脚本你会看到它先检索出最相关的两条知识片段再让模型基于片段回答。这里有几个实战中容易踩的坑一是切片策略不要把整篇文档塞进去按语义段落切每片 200-500 字比较稳二是嵌入模型要和对话模型分开配嵌入用专门的 embedding 模型对话用 chat 模型三是检索质量top_k不要设太大2-3 条足够太多反而会稀释关键信息。3.3 Function Calling让模型真正能“动手”Function Calling 的核心是你提前把可用函数的文档名称、参数、描述告诉模型模型根据用户问题决定调用哪个函数、传什么参数你的程序负责真正执行再把结果返回给模型模型最后组织成自然语言。下面是一个完整的天气查询示例函数本身用 mock 数据模拟import json # 1. 定义函数文档 tools [ { type: function, function: { name: get_weather, description: 查询指定城市的当前天气, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京, } }, required: [city], }, }, } ] # 2. 真正的函数实现 def get_weather(city): mock {北京: 晴25℃, 上海: 多云28℃, 深圳: 阵雨30℃} return mock.get(city, f{city}暂无数据) # 3. 调用流程 def chat_with_tools(user_input): messages [{role: user, content: user_input}] resp client.chat.completions.create( modelos.getenv(TAOTOKEN_MODEL), messagesmessages, toolstools, tool_choiceauto, ) msg resp.choices[0].message if msg.tool_calls: for call in msg.tool_calls: args json.loads(call.function.arguments) result get_weather(args[city]) messages.append(msg) messages.append({ role: tool, tool_call_id: call.id, content: result, }) final client.chat.completions.create( modelos.getenv(TAOTOKEN_MODEL), messagesmessages, ) return final.choices[0].message.content return msg.content print(chat_with_tools(北京今天天气怎么样))这段代码跑通后你会看到模型自动识别出要调用get_weather提取出city北京拿到结果后再组织成自然语言回答。这里的关键是tool_choiceauto让模型自己决定要不要调函数。如果你发现模型该调的时候不调可以把函数描述写得更明确或者在 system prompt 里加一句“涉及实时数据时必须调用工具”。4. 验证请求与成功结果MCP 接入与 Agent 组装三大件分别跑通后接下来做两件事用 MCP 把工具接入标准化然后把它们组装成一个能自主规划的 Agent。4.1 MCP 接入让工具调用有统一协议MCP 解决的是 Function Calling 没有统一标准的问题。没有 MCP 时每个人写的函数传输格式都不一样接入成本很高。MCP 把主机、客户端、服务端三个角色分清楚主机负责和用户、模型交互客户端寄生在主机里服务端对接外部数据三者通过标准协议通信。先装依赖pip install mcp一个最小的 MCP 服务端示例from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent app Server(demo-server) app.list_tools() async def list_tools(): return [ Tool( nameget_weather, description查询指定城市的当前天气, inputSchema{ type: object, properties: { city: {type: string, description: 城市名称} }, required: [city], }, ) ] app.call_tool() async def call_tool(name, arguments): if name get_weather: city arguments[city] mock {北京: 晴25℃, 上海: 多云28℃} return [TextContent(typetext, textmock.get(city, 暂无数据))] async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ __main__: import asyncio asyncio.run(main())这个服务端通过 stdio 和客户端通信客户端启动时会自动拉取list_tools返回的函数文档注入到模型的 Prompt 里。这就是 MCP 相比裸 Function Calling 的最大优势——函数文档自动注入不用你手动拼 Prompt。如果你用的是 Claude Code 这类支持 MCP 的客户端配置方式是在 settings 里加一段{ mcpServers: { demo-server: { command: python, args: [/path/to/your/mcp_server.py], env: { TAOTOKEN_API_KEY: sk-你的密钥, TAOTOKEN_BASE_URL: https://taotoken.net/api } } } }注意这里的三件套要写全Base URL 是https://taotoken.net/apiKey 是你的密钥Model ID 在客户端配置里指定。如果你用的是 Cline 或 CC Switch 这类工具配置逻辑类似核心都是把 Base URL、Key、Model ID 三个字段填对。4.2 Agent 组装用 ReAct 范式把一切串起来Agent 的核心是 ReAct 循环思考Thought→ 行动Action→ 观察Observation→ 再思考。下面是一个最小可运行的 Agent它同时具备 RAG 检索和 Function Calling 能力import json SYSTEM_PROMPT 你是一个智能助理可以调用工具来回答问题。 可用工具 1. search_knowledge(query): 从知识库检索信息 2. get_weather(city): 查询城市天气 请按以下格式思考 Thought: 我需要做什么 Action: 工具名 Action Input: 参数 Observation: 工具返回结果 ...重复直到可以回答 Final Answer: 最终回答 def agent_run(user_input, max_steps5): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input}, ] for _ in range(max_steps): resp client.chat.completions.create( modelos.getenv(TAOTOKEN_MODEL), messagesmessages, stop[Observation:], ) text resp.choices[0].message.content messages.append({role: assistant, content: text}) if Final Answer: in text: return text.split(Final Answer:)[-1].strip() if Action: in text: action text.split(Action:)[1].split(\n)[0].strip() action_input text.split(Action Input:)[1].split(\n)[0].strip() if action search_knowledge: obs \n.join(retrieve(action_input)) elif action get_weather: obs get_weather(action_input) else: obs 未知工具 messages.append({role: user, content: fObservation: {obs}}) return 达到最大步数未能完成 print(agent_run(北京天气怎么样顺便查一下TaoToken的API地址))跑通这个 Agent你会看到它先调get_weather查天气再调search_knowledge查 API 地址最后把两个结果合并成最终回答。这就是从 Prompt 到 Agent 的完整闭环。5. 本篇常见报错排查401、local proxy failed、reading choices、OAuth这一节把我实际踩过的坑列出来你遇到报错可以直接对照。401 Unauthorized最常见的原因是 Key 没配对环境变量或者.env文件没被load_dotenv()正确加载。先打印os.getenv(TAOTOKEN_API_KEY)看是不是 None如果是检查.env文件路径和变量名拼写。另一个原因是 Key 复制时带了空格或换行strip 一下再试。local proxy failed / connection error这类报错通常是 Base URL 写错了。检查是不是写成了带 UTM 参数的地址API 地址必须是干净的https://taotoken.net/api。如果你在公司内网检查是不是有网络策略拦截换一个网络环境试试。reading choices 报错 / KeyError: choices这通常说明返回的 JSON 结构和你预期的不一样多半是请求根本没成功返回的是错误信息。先把resp整个打印出来看而不是直接取resp.choices。常见原因是 Model ID 写错了模型不存在时返回体里没有choices字段。OAuth 相关报错如果你用的是 Claude Code 或 Codex 这类需要 OAuth 的客户端报 OAuth 错误通常是认证流程没走完或者本地缓存的 token 过期了。先清掉本地缓存重新登录再检查 settings 里的 Base URL 和 Key 是不是填对了。Codex 的auth.json里要确保 Base URL 指向https://taotoken.net/apiKey 字段填你的 TaoToken 密钥。MCP 服务端启动失败检查command和args路径是不是绝对路径Python 环境是不是你装了mcp包的那个环境。如果报模块找不到用which python确认路径或者直接用虚拟环境的 python 绝对路径。Function Calling 不触发模型该调函数却不调先检查tools参数格式对不对tool_choice是不是auto。如果格式没问题把函数描述写得更具体或者在 system prompt 里明确要求“涉及实时数据必须调用工具”。6. 继续往下走把通道固定下来把模块拆开练这套全家桶跑通之后你会发现真正的难点不在单个模块而在模块之间的衔接和调试。我的建议是先把 TaoToken 的 Base URL 和 Key 固定成环境变量所有项目共用一份配置这样换模型、换项目时不用改代码然后把 RAG、Function Calling、MCP 分别写成独立可测试的模块每个模块单独跑通再组装出问题时能快速定位是哪一层挂了。如果你后面要长期做编码和 Agent 场景可以了解一下 Coding Plan它更适合高频调用和长链路任务https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。日常调试模型和验证 Prompt 效果用模型对话页就够了https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 遇到配置问题可以先翻文档。如果你用 Claude Code可以参考 https://taotoken.net/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_contentClaudeCodeAnthropicutm_campaignrewrite 里的接入说明。最后说一个我自己的习惯每次调通一个新模块就把它封装成一个函数参数只留输入和输出内部实现随便改。这样等你组装 Agent 的时候直接调函数就行不用关心底层是 RAG 还是 Function Calling。模块化做得好Agent 的调试成本会低很多。