拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【愚公系列】《MCP协议与AI Agent开发》004-大模型原理及MCP开发基础(LLM 在应用中的典型接口模式)

【愚公系列】《MCP协议与AI Agent开发》004-大模型原理及MCP开发基础(LLM 在应用中的典型接口模式)

1. 从一次“模型不听话”说起:LLM 在 MCP 与 AI Agent 中的典型接口模式

如果你正在做 MCP 协议相关的开发,大概率遇到过这种场景:你明明在 system 里写了“必须调用 get_weather 工具”,模型却回你一句“今天北京天气不错,建议出门带伞”。这不是模型笨,而是你没把 LLM 的接口模式用对。LLM 在应用中的典型接口模式,说白了就是三件事:怎么把任务说清楚(消息结构)、怎么让模型决定动手(函数调用)、怎么把结果接回来(消息回传)。这三件事串起来,才构成 MCP 协议驱动 Agent 调用链的底层骨架。

这篇内容面向想理解 MCP 协议如何驱动 Agent 调用链的开发者。我会从 Completion 与 Chat 两种接口模式讲起,重点落在函数调用(Function Calling)的完整往返流程上,给出可复制的接口配置片段,并用一次真实的函数调用往返验证端到端联调。中间会用到 TaoToken 统一 Key/API 通道,把模型调用、工具描述、消息回传这条链路跑通。你不需要有 Agent 框架经验,只要能跑 Python 就能跟下来。

先明确一个认知:MCP 协议本身不负责“生成”,它负责“调度”。真正决定 Agent 能不能正确调用工具的,是 LLM 接口层对函数描述的理解与结构化输出能力。所以理解 LLM 的接口模式,是写 MCP Server 和 Agent 编排逻辑的前置条件。下面从最基础的两种接口模式开始拆。

2. Completion 与 Chat 接口模式对比:MCP 协议下该选哪种接口

大模型的服务交互主要围绕 Completion 与 Chat 两种接口模式展开。二者共享生成式语言建模的底层机制,但在输入格式、交互结构和适用场景上有明显差异。理解这个差异,直接决定你后面写 MCP 工具描述时的消息组织方式。

Completion 接口接收一段连续的 Prompt 作为输入,不包含角色结构与消息历史。它适合短文本续写、摘要生成、格式化输出这类线性任务。结构简单、响应快,但缺乏对语义上下文的长期保持能力。你在做单次工具结果润色时可以用它,但一旦涉及多轮工具调用,它就会丢上下文。

Chat 接口采用多轮消息体结构,显式引入角色标签(system、user、assistant、tool),能够完整建模对话历史。它适合上下文保持、任务规划、多轮控制等复杂语义场景,也是 MCP 推荐使用的核心模型接口形式。原因很直接:MCP 的工具调用需要把“用户请求 → 模型决策 → 工具执行 → 结果回传 → 模型总结”这条链路上的每一环都记录成消息,而只有 Chat 接口的消息数组能承载这种结构化历史。

下面这张表把两种模式在 MCP 场景下的关键差异列清楚,你可以对照自己的任务选型。

对比维度Completion 接口Chat 接口
输入结构单段 Prompt 字符串messages 数组,含 role/content
角色支持无system / user / assistant / tool
上下文保持弱,需手动拼接强,原生多轮
函数调用支持一般不直接支持原生支持 tools/functions
MCP 适用性仅用于结果润色工具调度主通道
典型场景摘要、格式化Agent 规划、工具调用

在工程实现上,主流平台普遍提供兼容 OpenAI SDK 风格的标准接口,支持统一调用语法、可选流式输出、灵活上下文组织。这意味着你写 MCP Server 时,可以用同一套 SDK 语法对接不同模型,只要改 base_url 和 model 字段。这也是后面我用 TaoToken 统一通道的原因:把 Key 和 base_url 收敛到一处,MCP 工具描述和消息回传逻辑不用跟着模型换而重写。

一个最小的 Chat 接口调用长这样,注意 messages 数组里 system 和 user 的分工:

from openai import OpenAI client = OpenAI( api_key="<你的 TaoToken API Key>", base_url="https://taotoken.net/api" ) response = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": "你是一个 MCP 工具调度助手,只输出结构化调用意图"}, {"role": "user", "content": "帮我查一下北京今天的天气"} ], stream=False ) print(response.choices[0].message.content)

这段代码体现的是 Chat 接口的结构化优势:任务指令、上下文语义与用户输入分离组织。在 MCP 协议里,system 通常承载工具清单和调用约束,user 承载真实请求,assistant 承载模型决策,tool 承载执行结果。四类角色各司其职,调用链才清晰。如果你把工具描述塞进 user 里,模型很容易把它当成普通文本忽略掉,这是新手最常见的坑之一。

3. 可复制的函数调用配置片段:tools 描述与消息回传结构

函数调用是 MCP 驱动 Agent 调用链的核心。它的本质是:模型不直接执行函数,而是生成一个包含调用意图的结构化响应,由外部系统负责执行并回传结果,再由模型处理输出。这个“解耦”设计是 MCP 协议能标准化调度工具的前提。

要让模型正确生成调用意图,你必须提前提供函数名、参数定义及说明文档。下面是一份可直接复制的 tools 配置片段,我把它写成 JSON 结构,你可以直接放进请求体,也可以转成 Python 字典:

{ "model": "deepseek-chat", "messages": [ {"role": "system", "content": "你是一个 MCP 工具调度助手。当用户请求涉及天气时,必须调用 get_weather 工具,不要自行编造天气。"}, {"role": "user", "content": "查询今天北京的天气"} ], "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的实时天气,返回温度、天气状况和风力", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如:北京、上海" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "温度单位,默认摄氏度" } }, "required": ["city"] } } } ], "tool_choice": "auto" }

这份配置里有三个关键点,直接决定 MCP 调用链能不能跑通。

第一,description 要写“什么时候用”,而不只是“这是什么”。我见过太多人把 description 写成“天气查询函数”,结果模型在用户问“今天出门穿什么”时不敢调用。正确的写法是描述触发条件,比如“当用户请求涉及天气、温度、穿衣建议时调用”。

第二,parameters 里的 required 要精确。如果你把 city 标成 required,模型就必须从用户话里抽出城市;如果用户没说城市,模型会先反问而不是瞎编。这个约束是 MCP 工具调用可靠性的来源。

第三,tool_choice 设为 auto 让模型自己判断,设为具体函数名则强制调用。在 Agent 编排里,我通常先用 auto 观察模型决策,确认稳定后再按场景收紧。

当模型决定调用时,返回的消息结构里会多出一个 tool_calls 字段,而不是普通的 content。这个结构就是 MCP 协议里“调用意图”的载体:

response = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": "你是一个 MCP 工具调度助手。当用户请求涉及天气时,必须调用 get_weather 工具。"}, {"role": "user", "content": "查询今天北京的天气"} ], tools=[{ "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的实时天气", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"] } } }], tool_choice="auto" ) tool_call = response.choices[0].message.tool_calls[0] print(tool_call.function.name) # get_weather print(tool_call.function.arguments) # {"city": "北京"}

拿到这个结构化响应后,你的 MCP Server 或 Agent 执行器负责真正调用天气 API,然后把结果以 role=tool 的消息回传。回传时必须带上 tool_call_id,否则模型无法把结果和之前的调用意图对应起来。这一步是消息回传衔接的关键,漏了 id 就会出现“模型重复调用同一工具”的死循环。

回传结构如下:

messages.append(response.choices[0].message) # 带上 assistant 的 tool_calls messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": '{"city": "北京", "temp": 26, "weather": "晴", "wind": "3级"}' }) final = client.chat.completions.create( model="deepseek-chat", messages=messages, tools=tools ) print(final.choices[0].message.content)

到这里,一次完整的“用户请求 → 模型决策 → 工具执行 → 结果回传 → 模型总结”往返就闭合了。MCP 协议做的事情,本质上是把这套消息结构标准化,让不同工具、不同模型之间能互相识别。

4. 端到端联调验证:用 TaoToken 统一 Key 跑通一次函数调用往返

前面讲的是结构,这一节讲怎么真正跑起来。我用 TaoToken 作为统一 API 通道,原因是它把 Key 管理和 base_url 收敛到一处,MCP 工具描述和消息回传逻辑不用因为换模型而重写。你只需要在 https://taotoken.net/api 这个 base_url 下换 model 字段即可。

先准备环境。安装 OpenAI SDK,这是目前兼容性最好的调用方式:

pip install openai

然后到 TaoToken 控制台创建一个 API Key。地址是 https://taotoken.net/api-keys ,创建后复制保存。注意 Key 只在创建时完整显示一次,丢了就重新建。

接下来写一个完整的验证脚本,把函数调用往返跑通。这个脚本模拟了 MCP Server 收到工具调用后执行并回传的全过程:

from openai import OpenAI import json client = OpenAI( api_key="<你的 TaoToken API Key>", base_url="https://taotoken.net/api" ) tools = [{ "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的实时天气,当用户询问天气、温度、穿衣建议时调用", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"} }, "required": ["city"] } } }] messages = [ {"role": "system", "content": "你是一个 MCP 工具调度助手,涉及天气必须调用工具。"}, {"role": "user", "content": "查询今天北京的天气"} ] # 第一轮:模型决策 resp1 = client.chat.completions.create( model="deepseek-chat", messages=messages, tools=tools, tool_choice="auto" ) msg = resp1.choices[0].message print("模型决策:", msg.tool_calls[0].function.name, msg.tool_calls[0].function.arguments) # 模拟工具执行 args = json.loads(msg.tool_calls[0].function.arguments) tool_result = {"city": args["city"], "temp": 26, "weather": "晴", "wind": "3级"} # 第二轮:回传结果 messages.append(msg) messages.append({ "role": "tool", "tool_call_id": msg.tool_calls[0].id, "content": json.dumps(tool_result, ensure_ascii=False) }) resp2 = client.chat.completions.create( model="deepseek-chat", messages=messages, tools=tools ) print("最终回复:", resp2.choices[0].message.content)

运行后你应该看到类似输出:

模型决策: get_weather {"city": "北京"} 最终回复: 北京今天天气晴,气温 26 摄氏度,风力 3 级,适合外出。

如果你看到的是模型直接编造天气而没有 tool_calls,说明 system 约束不够强或 description 没写触发条件。如果你看到 tool_calls 但第二轮报错,多半是 tool_call_id 没对上。这两个问题下一节展开。

验证模型对话能力时,你也可以直接在 https://taotoken.net/model-chat 里手动发一条“查询北京天气”看模型是否返回结构化调用意图,用来快速判断是模型问题还是代码问题。长期做 Agent 编排的话,Coding Plan 更适合持续调试,地址是 https://taotoken.net/coding-plan 。

5. 常见报错排查:401、local proxy failed、reading choices、OAuth

这一节按真实报错来。你在 MCP 联调时遇到的错误,八成集中在这四类。

401 Unauthorized。最常见的原因是 Key 没带对或 base_url 写错。检查两点:api_key 是不是从 https://taotoken.net/api-keys 复制的完整值;base_url 是不是 https://taotoken.net/api ,注意不要多加 /v1 或漏掉协议头。如果你用的是环境变量,确认变量名和代码里读的一致。401 不会因为模型名写错而触发,所以看到 401 先查 Key 和地址,别去改 model。

local proxy failed。这个报错通常出现在你本地配了转发规则但目标地址不可达时。排查顺序:先确认 base_url 能通,用 curl 直接打一下 https://taotoken.net/api 看返回;再检查你的 HTTP 客户端有没有被系统级设置劫持。如果你在容器里跑,确认容器网络能出网。这个错误和模型无关,纯粹是链路问题。

reading choices 相关报错,典型如KeyError: 'choices'或NoneType has no attribute choices。这几乎都是响应体不是预期结构导致的。原因有两个:一是请求被网关拦截返回了 HTML 错误页,SDK 解析失败;二是流式模式下你按非流式解析。如果你开了 stream=True,必须用 for chunk in stream 迭代,不能直接取 choices。另外,当模型返回的是 tool_calls 时,message.content 可能是 None,你如果直接 print content 会看到 None,这不是报错但容易误判。正确做法是先判断 message.tool_calls 是否存在。

OAuth 相关报错。如果你在 Claude Code 或类似客户端里配置 MCP Server 时看到 OAuth 失败,通常是客户端把 MCP 的鉴权和服务端 API 鉴权混在一起了。MCP Server 自身的启动鉴权用客户端配置,而调用 LLM 的 Key 用 TaoToken 的 API Key,两者不要混。Claude Code 接入时,Base URL 填 https://taotoken.net/api ,Key 填 TaoToken 的 Key,Model ID 填 deepseek-chat 或你实际使用的模型名。这三件套缺一不可,只填 Key 不填 Base URL 会走到默认地址导致鉴权失败。

为了让你对照排查,我把四类错误整理成表:

报错关键词最可能原因排查动作
401 UnauthorizedKey 错误或 base_url 错误核对 api-keys 与 /api 地址
local proxy failed本地转发链路不通curl 直连 base_url 验证
reading choices响应非预期结构或流式解析错检查 stream 用法与 tool_calls
OAuth failed鉴权层混用分离 MCP 鉴权与 LLM Key

还有一个隐蔽的坑:模型返回 tool_calls 后,你回传 tool 消息时 content 必须是字符串。如果你传了 dict,某些 SDK 会静默丢弃或报序列化错误。统一用 json.dumps 转字符串最稳。另外 tool_call_id 必须和上一轮 assistant 消息里的 id 完全一致,复制时别漏字符。

如果你在 Cline 或 CC Switch 里配 MCP,记得把 Base URL、Key、Model ID 三件套都填全。Cline 的 MCP 配置里,模型通道走 TaoToken,工具通道走你本地 MCP Server,两者通过消息结构衔接,不要试图让 MCP Server 自己去调模型。职责分清,调用链才稳。

6. 把接口模式用进 MCP 调用链:接入文档与后续调试入口

走到这里,你应该已经跑通了一次完整的函数调用往返。回头看,LLM 在 MCP 与 Agent 场景下的接口模式其实就三层:Chat 接口负责承载多轮消息结构,tools 描述负责把工具能力翻译成模型能理解的约束,tool 消息回传负责把执行结果接回上下文。这三层对齐了,MCP 协议驱动的调用链就顺了。

实际项目里,我建议你把工具描述当成接口契约来维护。每加一个 MCP 工具,就同步更新 description 和 parameters,并在 system 里写清楚调用约束。模型不会读你的代码,它只读你给它的描述。描述写得越像“什么时候用”,调用就越准。

如果你要接着调试更多模型或工具组合,接入文档在 https://taotoken.net/doc ,里面有各语言 SDK 的 base_url 和参数说明。需要新建或轮换 Key 时到 https://taotoken.net/api-keys 。想快速验证某个模型对工具描述的理解能力,直接去 https://taotoken.net/model-chat 发一条带工具意图的请求看返回结构。长期做 Agent 编排和 MCP Server 开发的话,https://taotoken.net/coding-plan 更适合持续联调。

最后留一个实用习惯:每次改完 tools 描述,先跑一遍本文第 4 节的验证脚本,确认模型能稳定返回 tool_calls 再接入真实工具。这一步花两分钟,能省掉后面大量“模型不调用工具”的排查时间。

返回列表