LLM-203: 从MCP出发构建AI Agent应用架构的惨痛教训
摘要: 在AI Agent开发中,过早依赖MCP协议作为核心架构导致严重技术债。关键教训包括:1)MCP应作为插件而非架构核心,主流开发应基于OpenAI兼容API以提高跨模型兼容性;2)必须严格遵循消息格式规范,特别是tool_call_id的关联和assistant消息的完整性,否则会导致上下文断裂;3)工具调用结果需以结构化格式(含原始调用ID)回传LLM。建议优先采用OpenAI API标
凌晨 2 点,屏幕上第 4 次返回了那个该死的
tool_calls结构,而我只想让它输出一句简单的回答… 那一刻我终于明白:不是模型不够聪明,而是应用架构层次与 API 设计埋了太多坑。在 Agent 开发早期,忽视对 LLM API 产品文档的研究,代价是成倍的开发时间和凌晨 2 点的崩溃。
前半年,在 MCP 协议火爆的一塌糊涂的时候,团队开启了基于 MCP 的 AI 应用研发,期待利用 MCP Server 和 Prompt Engineering 实现 AI 应用自主解析语意、规划执行与效果评估,摆脱讲话的定制 Workflow 模式应用。现实却很骨感,围绕着 MCP Client 与 Server 构建起 AI 应用交互架构终成开发绊脚石,而非助推器。
核心痛点在于 MCP 于 Agent 的逻辑关系不清,鲜有模型供应商主动兼容 Anthropic API,导致模型接入层设计应对供应商间迁移能力有限:
-
MCP 与 Agent: MCP Client 与 Server 不应该作为 AI 应用架构设计中的核心,从逻辑上将 MCP 视为 AI Agent 的插件更为合理,围绕着 MCP Client->LLM->MCP Server 无法构建良好的设计架构。
-
兼容性问题: 不同模型提供商(如 OpenAI, Anthropic, 国内大厂)的 API 调用方式、参数命名、返回结构存在巨大的差异,API 转换的成本高。MCP 示例展示了解析 Anthropic API 进行 Tool Call 应用,以此为基础构建起的 LLM API 抽象转换适配层更复杂、面对不同的 LLM 供应商难以实现统一调试。
教训一: 官方社区 MCP Client 演示架构示例不是 AI Agent 设计架构蓝本
-
LLM 标准: MCP Client 官方示例采用的 Anthropic API,与 Ollama、OpenAI API 设计内容(如消息格式、工具调用、角色定义)相去甚远,而且在 openai API 已被广大 LLM 供应商广泛提供兼容接口的情况下,AI Agent 开发应该尽量采用类 openai API 或类似其 API 的 LLM 抽象层。
-
工具使用: MCP 协议的流行已经使得其 Tool Call 使用方式成为事实标准,开发者在轻量级 Agent 开发过程中需重视 Tool Call 格式转换,这样有助于适配不同的 LLM 供应商。
-
框架选择: 在最初围绕着 MCP client 构建 AI 应用时,没有选择框架,赋予开发者自身更大的自由度,但是在处理规划、执行、反思处理业务时遇到了挑战,后来引入轻量级框架 PocketFlow(100 行代码),兼顾了开发的自由与框架的结构性。当然,大型的、复杂的 AI Agent 应用可以考虑与 LangChain、LlamaIndex 等流行框架和工具链深度集成,规范应用价格、降低开发实施成本。
-
OpenAI API: 目前主流模型供应商如 Ollama、Deepseek、阿里云百炼等,都提供了 openai 兼容接口,开发者掌握了 openai API 使用凡事,即可快速对接大多数 LLM 供应商商的后端模型(只要它们提供兼容 API),极大提升开发效率和可维护性。
因此,在技术构建的早期阶段,必须在充分调研 MCP 与 AI Agent 架构关系的基础上进行设计,并将“提供或使用兼容 OpenAI API 的 LLM”作为决策的核心考虑点。 这是 Agent 应用项目能否顺利实施、LLM 迁移和功能迭代的关键因素。
教训二:LLM 生成的内容加入 Messages,再次请求 LLM 时需完整保留
AI Agent 与 LLM 和 MCP/Tool Call 交互流程如下:
情景描述:
在使用 Ollama 中 OpenAI 兼容 API 的 qwen3:0.6b 处理请求,第一轮 LLM 返回的 Tool Call,调用结果生成的 Message(role=‘tool’)未设置tool_call_id,导致解析 Message 出错。
原因剖析:
- OpenAI 处理请求 Message 的 API 中,采用数据结构
ChatCompletionToolMessageParam对 role='tool’的条目进行解析,tool_call_id是一个必须字段。 - Message(role=‘tool’)中的
tool_call_id,可以将工具调用结果与上次 LLM 返回生成的 Message 中 tool call 内容结合,共同作为有效的 Context 被解析,,从而避免再次返回相同的工具调用(tool_calls)。
解决方法:
- LLM 生成消息: LLM 请求 Message 条目中,包括先前 LLM 返回 Message(对 role=‘assistant’)条目时,需保证消息的内容完整性。
- 工具生成消息: 调用 Tool Call 处理后的内容生成 LLM 请求 Message 时, 设置
role='tool',使用 openai API 则必须包含对应 LLM Tool call 的tool_call_id。
# 错误示例
processed_content = "北京当前天气:晴,气温 25°C,湿度 45%,东南风 2级。"
messages.append({
"role": "tool",
"content": processed_content, # 简洁、关键的信息!
"tool_call_id": tool_call_id
})
# 正确示例
messages.append({
"role": "tool",
"content": processed_content, # 简洁、关键的信息!
"tool_call_id": tool_call_id # 必须匹配之前的 call id!
})
关键点: tool_call_id 是 OpenAI API 用来关联工具调用请求(tool_calls)和工具执行结果(tool message)的唯一标识符。它告诉模型:“这个工具消息是对你之前那个特定工具调用请求的回应”。缺少或错误的 tool_call_id 会导致模型无法正确关联上下文!
教训三:LLM 输入与输出中的 Assistant Message
OpenAI API 的 messages 列表包含不同角色的消息 (system, user, assistant, tool),输入的assistant 角色的消息容易出错,另一个就是tool对assistant消息的依赖(错误日志: Invoke the chat completions API with the function response appended to the messages list)。
LLM 的输出结构:
当 LLM 可能发起工具调用时,它的响应结构是特殊的:
{
"id": "chatcmpl-...",
"object": "chat.completion",
"created": 1712345678,
"model": "gpt-4-turbo",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "...", // 可能为null!
"tool_calls": [{ // 关键!包含工具调用请求
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_current_weather",
"arguments": "{ \"location\": \"Beijing\" }"
}
}]
},
"logprobs": null,
"finish_reason": "tool_calls"
}],
"usage": {...}
}
注意:content 可能为 null,而 tool_calls 数组包含了调用的详细信息。
常见的致命错误:
- 给模型输出的Message 提示为包括上次返回的 content、think 等内容:
# 错误示例:试图“引导”模型调用工具,但错误使用了输出结构
messages.append({
"role": "assistant",
"content": None, # 人为设置null
"tool_calls": [{ # 人为构造一个call
"id": "fake_id_123",
"type": "function",
"function": {
"id": "call_abc123",
"name": "search_web",
"arguments": "{}" # 空参数,期望模型填充?
}
}]
})
这会导致 openai 接口下,模型行为对上下文理解混乱! 模型看到这个 assistant 消息无法被认定为已经处理的消息,持续在 Plan 到 Tool Call 之间进行多次重复循环。
修订方案:
- 请求中
assistant角色消息采用上次 LLM 处理返回内容中输出的消息结构。 - 请求中
tool角色消息之前必须跟着assistant角色消息,参见OpenAI 示例文档。
总结:Agent 开发的血泪箴言
- MCP: MCP 只是工具调用协议,是丰富 AI 应用功能的插件,不是构建 AI 应用架构的核心。
- LLM API 兼容性: AI 应用开发应该选择主流 LLM API 库,或为多数 LLM 供应商兼容的OpenAI API。无论是选择底层模型服务还是设计自研接口,这能避免无尽的内耗。
- 敬畏 API 细节:
- 对待 LLM 的输入消息结构和输出消息结构要超越 API 本身代码,从实际应用示例出发构建可用的
assistant的tool_calls输入消息。 tool_call_id是黄金纽带,必须确保其在assistant(request) 和tool(response) 消息间精确匹配。- 严格遵守
tool消息的上下文依赖关系(紧跟其对应的assistant请求消息)。
- 对待 LLM 的输入消息结构和输出消息结构要超越 API 本身代码,从实际应用示例出发构建可用的
- 深入骨髓的理解: 做 AI Agent 项目,必须投入大量时间精读、实践并深刻理解 LLM(尤其是 OpenAI)的 API 文档。 魔鬼藏在细节里,对 API 交互机制的肤浅认知必然导致项目在后期陷入泥潭。
Agent 的潜力巨大,但通往稳定可靠之路布满荆棘,忽视 API 兼容性与细节,代价是高昂的重构与无尽的调试之夜。 从基于 MCP 构建的 AI Agent 应用架构的惨痛教训出发,拥抱标准,深耕细节,才能让 Agent 开发从 Demo 走向落地,这不仅仅是技术选型,更是工程效率与项目成败的分水岭。
更多推荐
所有评论(0)