Agent、MCP Server与LLM交互关系全解析
内容整理自与 GLM-5.2 的深度技术对话。
首轮对话解析
MCP 协议是用于 Agent 和 MCP 服务器之间的调用吗?LLM 不具备直接调用 MCP 服务器的能力?
精准命中核心。MCP 协议本质上定义了 Agent(即 LLM 的宿主应用)与 MCP 服务器之间的通信契约。它遵循标准的客户端-服务器模型——Agent 作为宿主运行 MCP Client,与提供具体工具、数据源及服务的 MCP Server 建立双向、实时的数据通道。
你的判断完全正确:LLM 本身无法直接调用 MCP 服务器。LLM 本质上是一个纯推理引擎,缺乏发起网络请求或进程间通信的能力。实际流转中,必须由 Agent(编排层)担任桥梁角色:
- LLM 根据上下文生成调用意图,例如 Function Calling 请求。
- Agent 内的 MCP Client 捕获该意图,并将其转换为符合 MCP 协议规范的结构(底层通常采用 JSON-RPC 2.0)。
- MCP Client 向 MCP Server 发起请求,执行工具发现或工具调用。
- MCP Server 执行完毕后返回结构化结果,Agent 将该结果注入 LLM 的上下文,供其进行后续推理与回复生成。
一句话总结:MCP 解决了工具标准化接入与调用的架构问题。所有协议层面的通信与工具执行在 Agent 与 MCP Server 之间完成,LLM 仅负责理解请求与处理最终输出。
第二轮对话详解
接下来三个核心问题:
- LLM 如何知晓可调用的工具列表?
- LLM 如何向 Agent 传达具体的调用指令?
- LLM 如何获取工具执行后的结果?
- LLM 感知工具的机制
LLM 本身不具备预知能力,完全依赖 Agent(MCP Client)提供的上下文。Agent 运行时通过 MCP 协议从已连接的 MCP Server 动态拉取可用工具清单,包括工具名称、功能描述、入参定义等。随后,Agent 将这些工具定义格式化后注入 Prompt 或 API 请求的特定参数(如tools列表)。LLM 读取这些结构化信息后,便“了解”当前环境中可用的工具集。 - LLM 传达调用指令的方式
LLM 利用原生的 Function Calling 能力传达执行意图。接收到用户问题并分析可用工具后,若判断需要调用某工具,LLM 会在响应中生成一段结构化数据(通常是 JSON),明确指定工具名称及其入参。这段输出是机器可读的指令,而非自然语言,Agent 可直接解析并执行。 - LLM 获取执行结果的路径
工具的实际执行由 Agent 与 MCP Server 协同完成。Agent 解析出 LLM 的调用意图后,通过 MCP 协议向对应 Server 发起请求。Server 执行底层函数或 API 调用,将结构化结果返回 Agent。随后,Agent 将该结果以tool或function角色追加至对话历史,并再次向 LLM 发起请求。LLM 结合原始问题与刚获取的结果进行综合推理,最终生成面向用户的自然语言回复。
第三轮深度探讨
阅读第3点回答后产生新疑问:Agent 接收到 LLM 的函数调用意图并执行具体函数后,如何判断是将执行结果直接返回给用户,还是继续交由 LLM 进行二次推理?
参考以下与 LLM 交互的示例代码:
import json from zai import ZhipuAiClient # 初始化客户端 client = ZhipuAiClient(api_key='您的apikey') # 定义天气查询函数 def get_weather(city: str) -> dict: """获取指定城市的天气信息""" # 此处应调用真实天气 API weather_data = { "city": city, "temperature": "22°C", "condition": "晴天", "humidity": "65%", "wind_speed": "5 km/h" } return weather_data # 定义函数工具 tools = [ { "type": "function", "function": { "name": "get_weather", "description": "获取指定城市的当前天气信息", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如:北京、上海" } }, "required": ["city"] } } } ] # 发起对话请求 response = client.chat.completions.create( model="glm-5.2", # 选用支持函数调用的模型 messages=[ {"role": "user", "content": "北京今天天气怎么样?"} ], tools=tools, # 传入函数工具 tool_choice="auto" # 自动决策是否调用函数 ) # 处理函数调用 message = response.choices[0].message messages = [{"role": "user", "content": "北京今天天气怎么样?"}] messages.append(message.model_dump()) if message.tool_calls: for tool_call in message.tool_calls: if tool_call.function.name == "get_weather": # 解析参数并调用函数 args = json.loads(tool_call.function.arguments) weather_result = get_weather(args.get("city")) # 将函数结果回传模型 messages.append({ "role": "tool", "content": json.dumps(weather_result, ensure_ascii=False), "tool_call_id": tool_call.id }) # 获取最终回答 final_response = client.chat.completions.create( model="glm-5.2", messages=messages, tools=tools ) print(final_response.choices[0].message.content) else: print(message.content)该程序直接将函数结果暴露给用户。若 LLM 需基于函数结果进一步推理总结,应如何设计?
关键逻辑在于 LLM 返回的结束信号与 Agent 编排层的循环控制。先说一个细节:你提供的代码实际上已经实现了“交由 LLM 继续推理”的流程,并未将原始结果直接输出给用户。 我们逐步拆解:
1. 机制解析:结果去向的判定依据
标准 Function Calling 机制中,LLM 的响应携带明确的完结标志(Finish Reason)或动作指令:
- 需要继续推理(返回 tool_calls):LLM 判断需调用外部函数时,响应中返回
tool_calls字段,完结标志通常为tool_calls。这意味着 LLM 要求执行工具并将结果回传,以便继续处理。 - 直接输出(返回纯文本):当 LLM 已获取工具结果或认为无需调用工具即可回答时,它直接生成自然语言文本。响应中不包含
tool_calls,完结标志为stop。这意味着 LLM 已生成最终回复,可直接呈现给用户。
2. 代码实际运行逻辑复盘
你提供的代码片段正是标准的“交由 LLM 总结”流程:
# 步骤一:检查 LLM 是否发出工具调用指令
if message.tool_calls:
for tool_call in message.tool_calls:
if tool_call.function.name == "get_weather":
# 步骤二:Agent 解析并执行函数,获取原始结果
args = json.loads(tool_call.function.arguments)
weather_result = get_weather(args.get("city"))
# 步骤三:将原始结果以 role="tool" 形式追加至对话历史
messages.append({
"role": "tool",
"content": json.dumps(weather_result, ensure_ascii=False),
"tool_call_id": tool_call.id
})
# 步骤四:发起新请求,让 LLM 基于完整上下文(含 tool 结果)进行总结
final_response = client.chat.completions.create(
model="glm-5.2",
messages=messages,
tools=tools
)
# 步骤五:打印 LLM 总结后的最终回复
print(final_response.choices[0].message.content)
这段代码中,weather_result(包含温度、湿度等原始字典)并未被直接 print。程序将其回传 LLM,然后打印 final_response——这是 LLM 消化原始数据后生成的格式化自然语言回复(例如:“北京今日天气晴朗,气温22度...”)。
3. 若需“直接输出”应如何实现?
作为对比,若希望 Agent 执行函数后直接将原始结果返回用户,代码应调整为:
if message.tool_calls:
for tool_call in message.tool_calls:
if tool_call.function.name == "get_weather":
args = json.loads(tool_call.function.arguments)
weather_result = get_weather(args.get("city"))
# 直接输出原始结果,不再发起二次 LLM 请求
print(json.dumps(weather_result, ensure_ascii=False))
4. 成熟 Agent 框架的控制逻辑
在生产级 Agent 开发中(如 LangChain 的 AgentExecutor),这个判定过程被抽象为自动化循环:
- Agent 接收 LLM 输出。
- 判断输出类型:
- 若为
AgentAction(对应tool_calls),则执行工具,将结果拼入上下文,继续循环。 - 若为
AgentFinish(对应stop),则跳出循环,将结果返回用户。
- 若为
- 防死循环机制:为规避 LLM 无限请求调用工具,框架通常设置
max_iterations(最大迭代次数)。超限后,Agent 强制中断并将当前结果返回用户。
第四轮框架价值分析
据此可知编写基础 Agent 并不复杂,那么 Agent 框架的核心价值是什么?此外还提供了哪些增强功能?
确实,构建一个最小化 Agent 原型仅需 LLM 加函数调用循环。但一旦进入生产环境,工程复杂度骤升。Agent 框架的真正意义在于帮你从“原型验证”跨越到“产品级系统”。
一、Agent 框架解决的核心痛点
- 状态与记忆管理
简单脚本中,对话历史靠messages列表拼接。但长周期任务中,历史记录会迅速突破 LLM 上下文窗口。框架提供自动截断、滑动窗口、摘要压缩等策略,并支持接入向量数据库实现长期记忆(RAG),避免 Agent 在长对话中“失忆”。 - 复杂控制流与编排
基础 Agent 是线性流程(LLM → 工具 → LLM)。真实任务往往需要更复杂的逻辑:- 并行调用:用户提问“对比北京和上海天气”,框架可自动触发两个并行工具调用,而非串行等待。
- 条件分支与重试:任务失败时自动重试,或根据工具结果动态决策下一步分支。
- 子任务分发:将复杂任务拆解为子图,分配至不同专用 Agent 处理。
- 健壮性与异常处理
手写代码时,API 超时、LLM 返回的 JSON 格式错误或无限循环将直接导致崩溃。框架内置兜底机制:工具重试、格式修复、强制迭代限制等。 - 供应商解耦
若手写对接智谱 API,后续切换至 OpenAI 或开源模型需重写大量底层代码。框架提供统一抽象接口,仅需修改一行配置即可无缝切换 LLM 提供商。
二、Agent 框架的增值功能
除解决上述痛点,框架还提供一系列高级能力,显著提升开发效率:
- 预置认知架构
框架内置经过业界验证的推理引擎,无需从零设计 Prompt 逻辑。例如:- ReAct:交错生成推理轨迹与动作。
- Plan-and-Execute:先制定全局计划,再逐步执行。
- Tree of Thoughts (ToT):树状探索多条求解路径,支持回溯。
- 多智能体协作
单个 Agent 难以覆盖所有领域。框架(如 AutoGen、MetaGPT、CrewAI)支持定义不同角色的 Agent(如“产品经理 Agent”、“开发 Agent”、“测试 Agent”),它们可在框架内自动对话、交接任务、交叉审查代码,完成端到端复杂工作流。 - 生态丰富的工具集成
框架提供开箱即用的工具生态(搜索引擎、数据库查询、文件读写、代码执行器),并原生集成各类第三方 API。同时,现代框架正积极适配 MCP 协议,标准化接入无限外部工具源。 - 人机交互机制
执行敏感操作(如发送邮件、删除数据库)前,框架可暂停流转,等待人工审批后再继续。 - 可观测性与调试工具
Agent 行为常被视为“黑盒”。框架提供 LangSmith 等追踪工具,可可视化每次 LLM 调用的输入输出、工具调用耗时、Token 消耗情况,对 Prompt 调优与成本控制至关重要。
总结
手写 Agent 如同用原生 Socket 手写 HTTP 请求——能跑通,但繁琐且难维护;使用 Agent 框架则类似采用 Web 框架(FastAPI 或 Spring Boot),它帮你处理路由、中间件、并发、异常处理等基础设施,让你聚焦于业务逻辑(工具定义与 Prompt 设计)。
第五轮技术细节问答
最后三个问题:
- Agent 与 LLM 间的网络请求使用何种协议?长连接还是无状态短连接?
- 随着对话历史增长,每次请求的 body 是否持续膨胀?
- 超出上下文窗口时,compact 操作由 Agent 还是 LLM 执行?
以下是针对三个问题的详细解析:
1. 协议类型与连接模式
Agent 与 LLM 的通信协议取决于模型提供商及底层架构,当前正处于从无状态短连接向有状态长连接的演进阶段:
- 主流现状:基于 HTTP 的请求-响应模式(应用层无状态)
目前大多数大模型 API(如 OpenAI、智谱标准接口)基于 HTTP/1.1 或 HTTP/2。尽管 HTTP/1.1 默认开启Keep-Alive复用 TCP 连接以减少握手开销,但应用层逻辑仍为无状态的短连接交互。即:Agent 发送 POST 请求 → 服务器流式或非流式响应 → 逻辑上本次事务结束。Agent 必须在下次请求中重新携带完整上下文(历史记录),服务器端不保存会话状态。 - 前沿趋势:WebSocket 模式(有状态长连接)
随着 Agent 工作流变长(涉及多步工具调用),HTTP 每次请求携带大量 Header 与重复上下文导致的“控制平面延迟”愈发显著。2026年初,OpenAI 等厂商已为 Responses API 推出 WebSocket 模式。该模式建立持久化有状态连接,Agent 与 LLM 可双向、持续推送消息,服务器维护会话状态,大幅优化 20+ 次工具调用等长链 Agent 场景的执行效率。
2. 请求 body 的膨胀特性
在传统无状态 API 模式下确实如此。
LLM 本身无状态,Agent 必须在每次请求中将历史对话(用户提问、LLM 回复、工具调用记录及结果)全部打包进 messages 数组。随着对话轮数增加,该数组线性膨胀,导致:
- 网络传输带宽消耗增加。
- Token 计费成本上升(按输入与输出总 Token 计费)。
- 推理首字延迟增加(LLM 需先处理庞大输入才能开始生成)。
(注:若采用 WebSocket 有状态连接,Agent 可仅发送增量消息,由服务器维持会话状态,避免 body 持续膨胀。)
3. Compact 操作的执行者
此操作由 Agent(及底层框架/编排逻辑)发起并控制,LLM 仅被动配合执行。
LLM 是被动文本生成器,无法感知自身上下文窗口,更无法主动触发压缩。标准 compact 流程如下:
- 监控与判定(Agent 完成):Agent 每次发送请求前,使用 Tokenizer 统计当前
messages的总 Token 数。若超出目标 LLM 的上下文窗口限制(如 8K、128K),Agent 触发 compact 机制。 - 执行策略(Agent 控制,可能调用 LLM):
- 简单截断(Agent 独立完成):Agent 按 FIFO 原则删除最旧的若干轮对话,或直接裁剪体积庞大的旧工具返回结果。
- 记忆摘要(Agent 调用 LLM 完成):Agent 截取一段较早的历史对话,构造 Prompt(如:“请将以下对话总结为关键上下文信息”),发起额外、独立的 LLM 请求。获取 LLM 返回的摘要文本后,Agent 用其替换庞大旧历史,再拼接最近对话,形成新的、更紧凑的
messages发送给 LLM 进行正常推理。
因此,Compact 是 Agent 为适应 LLM 上下文窗口限制而主动实施的“记忆管理”工程手段。
第六轮Token消耗分析
对话过程中 messages 数组线性增长,是否意味着每次提问消耗的 Token 都比上一次更多?
完全正确。在传统无状态 API 模式下,随着对话轮数增加,每次提问的输入 Token 数量确实持续增长,且总消耗呈二次方级攀升。
具体逻辑如下:
1. 输入 Token 的线性累加
LLM 无状态,Agent 每次请求必须附带全部历史记录。例如:
- 第 1 次提问:发送 1 条消息(当前提问)。
- 第 2 次提问:发送 3 条消息(第1轮用户提问 + 第1轮LLM回答 + 当前提问)。
- 第 3 次提问:发送 5 条消息(前两轮历史 + 当前提问)。
2. 累计 Token 消耗呈二次方级爆炸
单次输入 Token 线性增长,但整个对话过程的总消耗呈 \(O(N^2)\) 增长。假设每轮对话固定产生 100 Token:
- 第 1 轮:输入 100,输出 100。
- 第 2 轮:输入 200(含第1轮历史),输出 100。
- 第 3 轮:输入 300(含前2轮历史),输出 100。
- ...第 N 轮:输入 100N,输出 100。
仅进行 10 轮对话,累计输入 Token 达 5500,而非 1000。这就是 Agent 场景中 Token 消耗远超预期的原因。
3. Agent 场景下的“超级膨胀”
在普通对话中,增长可预测。但在 Agent 场景中,膨胀更剧烈:每次工具调用至少新增两条消息(LLM 的调用指令 + 工具返回结果)。若工具返回长文档或代码,该部分 Token 会被完整带入后续每次循环,导致上下文体积瞬间飙升。
应对策略
为避免 Token 暴涨导致成本失控或超出上下文窗口,通常采取以下措施:
- Agent 端 Compact:历史记录达到阈值时,触发摘要压缩,用简短总结替换冗长早期对话。
- 采用有状态 API:利用新一代大模型 API(如 WebSocket 模式)的服务端状态保持能力,仅发送增量消息。
- 输入端提纯:提交前自动清洗工具返回结果,剔除冗余字段与格式,仅保留核心数据,控制单轮交互体积。
