从Demo到生产:LLM Agent工程化实战与架构设计
1. 从“玩具”到“生产力”:Agent开发的现实困境与破局思路
最近和几个做AI应用的朋友聊天,大家不约而同地提到了一个词:“玩具感”。我们基于各种开源框架,吭哧吭哧地搭出了一个能对话、能联网、能调工具的Agent,Demo跑起来效果惊艳,但一旦想把它嵌入到真实的业务流里,或者交给非技术同事去用,问题就全暴露出来了。要么是回答的内容飘忽不定,这次能准确调用API查天气,下次就给你编一段天气预报;要么是处理多轮复杂对话时,前言不搭后语,完全忘了用户上一分钟说了什么;再或者,面对一个稍微复杂点的用户请求,Agent就像陷入了死循环,在几个工具之间来回横跳,就是给不出最终答案。
这其实就是当前很多Agent项目从“技术演示”迈向“生产级应用”时遇到的核心瓶颈。我们手里握着一堆闪闪发光的组件:大语言模型(LLM)是大脑,记忆(Memory)是海马体,工具(Tool)是手脚,检索增强生成(RAG)是外部知识库,模型上下文协议(MCP)是神经接口,技能(Skills)是肌肉记忆。但如何把这些部件有机地组装起来,形成一个稳定、可靠、可用的智能体,而不是一个时不时就“死机”或“胡言乱语”的拼接怪,这才是真正的挑战。
今天这篇笔记,我不想再重复那些框架的基础用法,而是想结合我最近在几个真实项目中趟过的坑,聊聊在LLM + Memory + Tool + RAG + MCP + Skills这个经典架构下,如何做一些“接地气”的思考和设计,让Agent真正能扛事儿。你会发现,很多问题的答案不在最新的论文里,而在最朴素的工程实践和架构权衡中。
2. LLM作为“推理引擎”:选型、提示工程与稳定性驯服
LLM是Agent的绝对核心,它负责理解、规划、决策和生成。但把它当作一个“黑盒魔法”来用,是项目失控的开始。
2.1 模型选型:在成本、能力与延迟之间做残酷的权衡
很多人一上来就问:“用GPT-4还是Claude 3?” 这其实是个错误的问题。正确的问题是:“我的Agent需要处理的任务类型是什么,它的失败成本有多高?”
- 复杂规划与逻辑推理场景:如果你的Agent需要像项目经理一样拆解复杂任务(例如,“帮我规划一个三天的北京旅游行程,要兼顾历史、美食和购物,预算有限”),那么GPT-4、Claude 3 Opus这类顶级闭源模型或DeepSeek-V2、Qwen2.5-72B这类顶尖开源模型几乎是唯一选择。它们的思维链(CoT)能力更强,规划更可靠。这里的失败成本是用户得到一个混乱或无用的计划,体验直接归零。
- 标准化工具调用与信息提取场景:如果Agent的主要工作是遵循固定流程调用工具(例如,根据用户问题查询数据库、生成报告、发送邮件),那么GPT-3.5-Turbo、Claude 3 Haiku甚至一些优秀的7B-14B开源模型(如Qwen2.5-14B, Llama 3.1-8B)可能就足够了。通过精细的提示工程,它们能非常稳定地输出结构化的JSON来调用工具。此时,失败成本是偶尔的工具调用错误,可以通过重试或降级处理来弥补,而成本效益却大幅提升。
- 高并发、低延迟的轻量级交互场景:对于需要实时响应的客服助手或游戏NPC,延迟是关键。更小参数量的模型(如Phi-3-mini, Gemma-2B)或经过蒸馏的专用模型是更好的选择。你需要测试,在95%的常见问题上,小模型能否达到可接受的准确率。
我的实操心得:不要追求“一步到位”的顶级模型。采用分层模型策略。让一个轻量、快速的“路由模型”先对用户意图进行分类:简单查询走小模型,复杂规划走大模型,工具调用走中等模型。这能极大优化综合成本与体验。我们一个内部服务Agent,用Qwen2.5-7B做路由和简单响应,只有约15%的复杂任务才路由到GPT-4,月度成本降低了70%,而终端用户几乎无感。
2.2 提示工程:超越“零样本”,构建可预测的思维框架
“请扮演一个助手…”这种提示在Demo里还行,在生产环境里太脆弱了。生产级的提示是一个精密的“控制程序”。
强制结构化输出:这是稳定性的基石。永远要求LLM以指定格式(如JSON、XML)输出。这不仅是为了方便程序解析,更是为了约束LLM的“胡思乱想”。
// 一个工具调用指令的强制格式 { "thought": "用户想查北京明天的天气,我需要调用天气查询工具。", "tool": "get_weather", "tool_input": { "city": "北京", "date": "tomorrow" } }使用Pydantic或JSON Schema在调用前就定义好输出结构,许多框架(如LangChain, LlamaIndex)都支持,能有效减少输出格式错误。
提供“少样本”范例,而非泛泛而谈:与其说“请仔细思考”,不如直接给它一个同类问题的完整思考过程示例。这比任何描述都有效。
用户:我想去上海出差,帮我看看这周末的航班和酒店。 助理思考过程: 1. 目标分解:这是一个多步骤任务,需要先查航班,再根据航班时间查酒店。 2. 信息确认:需要用户出发城市、具体日期(本周末指周六出发周日回?)、预算偏好。 3. 工具规划:先调用 flight_search,再调用 hotel_search。 4. 行动:先询问用户确认信息。设计“安全护栏”提示:在系统提示词中明确写入负面指令,比如“你绝对不能代替用户执行支付操作”、“如果用户询问的知识不在提供的资料中,你必须明确告知不知道,并拒绝编造”。这能预防很多越界行为。
2.3 应对“幻觉”与不稳定性:工程兜底方案
LLM天生会“胡扯”,我们必须接受这一点并设计兜底机制。
- 重试与回退:如果LLM的输出无法解析,或调用的工具返回了意外错误,不要直接向用户报错。设计一个重试循环(例如最多3次),每次重试时,将错误信息作为上下文重新提示LLM。如果重试失败,则回退到一个预设的安全回复(如“当前服务繁忙,请稍后再试”或引导用户简化问题)。
- 置信度过滤:对于一些关键事实(尤其是从RAG中检索到的),可以让LLM对自己生成答案的置信度进行评分。如果低于阈值(如70%),则触发人工审核流程或直接回复“我对此不太确定,建议您查阅官方文档”。
- 输出验证与清洗:在最终回复呈现给用户前,用一套简单的规则(如关键词过滤、敏感词检测)或另一个轻量级模型对内容进行最后的安全和合规性检查。
3. Memory设计:短期、长期与会话,如何不让Agent“失忆”
Memory决定了Agent的连续感和个性化能力。一个没有记忆的Agent,每一轮对话都是“初见”。
3.1 短期记忆(会话记忆):不仅仅是保存聊天记录
短期记忆通常指当前会话的上下文。最朴素的做法就是把所有历史对话都塞进上下文窗口。但这很快会遇到瓶颈(上下文长度限制、成本增加、无关信息干扰)。
- 关键技巧:摘要式记忆:不要原封不动地存储每一轮对话。当对话轮数达到一定阈值(如10轮)或上下文长度接近限制时,触发一个摘要动作。让LLM用一两句话总结本轮对话的核心事实、用户偏好和待办事项,然后用这个摘要替换掉早期的详细历史。这样,我们始终用有限的令牌数保存了对话的“精髓”。
- 摘要时机:可以在每轮对话后异步进行,也可以在设计对话流程时,在自然段落结束时(比如用户说“好的,那我们接下来定酒店吧”)主动触发摘要。
- 摘要内容:应包括:1) 已确认的事实(如“用户张三,计划5月20日-22日前往北京”);2) 用户的明确偏好(如“偏好高铁 over 飞机,预算中等”);3) 未完成的任务项(如“需要查询故宫门票 availability”)。
3.2 长期记忆(向量数据库):让Agent真正“认识”用户
长期记忆用于存储跨会话的、需要持久化的信息,比如用户的个人资料、历史交互习惯、曾经处理过的项目详情等。
- 存储什么?不是存原始对话。而是存结构化或半结构化的“用户事实”。
- 结构化信息:直接存入传统数据库(MySQL, PostgreSQL)。例如:
{user_id: 123, preferred_city: "上海", dietary_restriction: "素食", project_history: ["项目A-2023", "项目B-2024"]}。 - 非结构化信息:将值得记忆的对话片段(如“用户曾提到他对古典音乐特别感兴趣”)通过嵌入模型(Embedding)转换成向量,存入向量数据库(如Chroma, Pinecone, Weaviate)。检索时,用当前对话的向量去查找相关的长期记忆。
- 结构化信息:直接存入传统数据库(MySQL, PostgreSQL)。例如:
- 如何检索与激活?在每次对话开始时,或当LLM认为需要更多用户背景时(例如用户说“像上次那样安排就行”),自动从长期记忆中检索最相关的几条信息,作为上下文插入到当前对话中。这相当于给了Agent一个关于当前用户的“个人档案速览”。
3.3 记忆的更新与维护:避免信息过时与冲突
记忆不是只写不读的日志,它需要维护。
- 更新策略:当用户明确更正信息时(如“我不喜欢上海了,现在更喜欢杭州”),需要直接更新或覆盖长期记忆中的对应条目。对于偏好类信息,可以采用“加权衰减”或“最新优先”的策略。
- 冲突解决:如果从不同来源(如本次对话 vs 长期记忆)检测到信息冲突(例如,用户之前说爱喝咖啡,现在说要戒咖啡),优先采用本次对话中明确陈述的信息,并可以询问用户进行确认(“注意到您之前提到常喝咖啡,现在是想减少咖啡因摄入吗?”),根据确认结果更新记忆。
- 记忆的“遗忘”:为记忆设置TTL(生存时间)或重要性评分。一些临时性的、低重要性的记忆(如“用户今天问了天气”)可以在一段时间后自动清理,防止记忆库膨胀和检索噪音。
4. Tool与Skills的编排:从“能调用”到“会调用”
Tools是Agent的手臂,但让Agent知道在何时、用何种顺序、传递什么参数去挥舞这些手臂,是更大的挑战。
4.1 工具描述的“玄学”:清晰、具体、带示例
工具的描述(Description)是LLM决定是否调用以及如何调用的唯一依据。模糊的描述必然导致错误的调用。
- 反面教材:
search_web(query: str): 搜索网络。 - 正面教材:
好的描述要说明:1)工具用途(干什么用);2)适用场景(什么时候用);3)输入要求(参数格式和示例);4)能力边界(什么不能做)。search_web(query: str): 使用搜索引擎获取最新的、公开的网络信息。适用于查询实时新闻、体育比分、名人简介、未知概念等。参数`query`应是一个简洁明确的关键词或短语,例如“2024年巴黎奥运会开幕式时间”,避免使用长句或问题形式。此工具无法访问私人或付费墙后的内容。
4.2 动态工具选择与规划:解决“工具迷宫”问题
当工具数量众多时,LLM可能陷入选择困难或做出错误规划。
- 工具分组与路由:不要一股脑地把所有工具描述都塞给LLM。根据场景对工具进行分组。例如,一个“旅行规划Agent”可以分成【交通查询组】、【住宿查询组】、【景点信息组】。在对话开始时,根据用户意图先确定激活哪个工具组,只将该组的工具描述提供给LLM,大大降低了选择的复杂度。
- 分步执行与人工确认:对于涉及多个步骤、有潜在风险或高成本的操作(如“预订机票-预订酒店-租车”),不要让它一气呵成。设计工作流,让Agent在完成每一步后,将结果汇总给用户确认,再执行下一步。这既是安全措施,也让用户有掌控感。
- 工具调用结果的后处理:工具返回的往往是原始数据(JSON、HTML等)。LLM并不擅长直接理解这些数据。最佳实践是,为重要的工具编写一个专用的“结果解析器”。这个解析器将原始数据转换成一段自然语言摘要或更结构化的信息,再交给LLM进行后续处理和生成回复。这比让LLM去“阅读理解”一堆杂乱JSON要可靠得多。
4.3 Skills:可复用的复杂操作单元
Skill可以理解为一系列Tool和逻辑判断的预编排组合。比如,“预订航班”这个Skill,内部可能包含了search_flights、filter_by_price、get_seat_map、create_booking等多个工具的协调调用,以及一些业务逻辑(如“国际航班需提前至少2小时”)。
- Skill的设计哲学:一个Skill应该对应一个完整的、用户可理解的业务目标,而不是一个技术动作。它的存在,让LLM可以从更高、更稳定的层面进行规划(“用户要订机票,我执行‘预订航班’Skill”),而无需关心底层十几个工具的具体调用顺序和异常处理。
- Skill的封装:将Skill的实现封装成一个独立的函数或类,内部处理好工具调用序列、异常处理、结果合并。对LLM暴露的,只是一个清晰的Skill描述和简单的输入输出接口。
5. RAG的深度融合:知识库不是外挂,而是大脑延伸
RAG解决了LLM知识陈旧和幻觉的问题,但简单的“检索-拼接-生成”模式,在复杂Agent场景下常常失灵。
5.1 检索的时机与策略:主动还是被动?
- 被动检索(Reactive RAG):这是最常见的方式。当用户提问时,Agent将问题转换为查询向量,去知识库检索相关片段,然后生成答案。这适用于明确的问答。
- 主动检索(Proactive RAG / Agentic RAG):在Agent执行多步骤任务的过程中,自主判断何时需要检索知识。例如,在规划旅游行程时,当LLM打算推荐“颐和园”时,它可以主动触发一次检索,获取颐和园的最新开放时间、门票价格和游客评价,再基于这些信息进行推荐。这需要LLM具备一定的“元认知”能力,知道自己知识不足。
5.2 解决“检索质量”核心痛点:从嵌入到重排序
检索不到或检索不准,RAG就失败了。
- 查询改写(Query Rewriting):用户的原始问题可能模糊、口语化。在检索前,先用LLM对查询进行改写或扩展。例如,用户问“苹果那个新出的东西怎么样?”,可以改写成“Apple 最新发布的 iPhone 15 产品评测与用户体验”。
- 混合检索(Hybrid Search):不要只依赖向量检索(语义相似度)。结合关键词检索(BM25)。向量检索擅长处理“意思相似”,关键词检索擅长处理“字面匹配”。两者结合(如通过加权分数)能显著提升召回率。许多向量数据库(如Weaviate, Elasticsearch)已原生支持。
- 重排序(Re-ranking):初步检索可能返回几十个片段,其中只有前几个是真正相关的。使用一个更小、更快的重排序模型(如BGE-Reranker, Cohere Rerank)对Top N的结果进行精排,根据与问题的相关性重新排序,将最相关的3-5个片段喂给LLM。这一步成本增加很小,但效果提升巨大。
- 元数据过滤:为知识库文档添加丰富的元数据(如文档类型、创建日期、部门、产品线)。检索时,除了语义匹配,还可以用元数据进行过滤。例如,“只检索2024年发布的、属于‘财务政策’类别的文档”。这能极大提升精度。
5.3 让LLM“知道”它知道什么:引用与置信度
当Agent基于RAG生成答案时,必须告诉用户信息的来源。
- 引用溯源:在最终答案中,以脚注或括号的形式标明引用的文档名称或编号。例如,“根据《2024年公司差旅政策V2.1》第3条规定…”。这增加了可信度,也方便用户追溯。
- 处理“无答案”情况:如果检索到的知识片段都无法回答用户问题,要设计LLM的回复话术,如“根据我现有的知识库,未能找到关于XX的确切信息。这可能是因为信息尚未更新,建议您通过其他渠道核实。”绝对禁止在这种情况下让LLM自由发挥、编造答案。
6. MCP的角色:连接异构世界的“标准插座”
模型上下文协议(Model Context Protocol, MCP)是近期一个非常值得关注的方向。你可以把它理解为智能体世界的“USB-C接口”标准。
6.1 MCP解决了什么痛点?
在没有MCP之前,每个工具、每个数据源都需要为不同的Agent框架(LangChain, LlamaIndex, AutoGen…)编写特定的适配器。一个公司内部的CRM系统,如果想被多个AI应用调用,集成成本很高。
MCP定义了一套标准协议,让任何资源(数据库、API、文件系统、甚至另一个服务)都可以以一个标准化的方式,向AI应用(或Agent)暴露其可查询和可操作的能力。资源提供方只需要实现一个MCP服务器(Server),任何兼容MCP的客户端(如Claude Desktop, Cursor IDE,以及未来更多的AI应用)就能立即发现并使用这些资源和工具。
6.2 在Agent开发中如何利用MCP?
对于Agent开发者而言,MCP带来了两个层面的便利:
- 快速集成外部能力:如果你需要让Agent能搜索网络、查询公司数据库、读取Notion文档,你不再需要去找特定的SDK或自己写API封装。你可以直接寻找或部署对应的MCP服务器(如
tavily-mcp用于搜索,postgres-mcp用于数据库,filesystem-mcp用于读文件)。你的Agent框架(如果支持MCP)就能像插拔U盘一样,动态加载这些能力。 - 封装和暴露自身能力:如果你开发了一个强大的内部Agent,你也可以将它的一些功能(如“生成周报”、“分析销售数据”)通过MCP服务器暴露出去。这样,其他AI应用(比如公司的智能助手)就能直接调用你这个“专家Agent”的服务,实现AI能力的模块化和复用。
6.3 当前实践与展望
目前,MCP主要由Anthropic推动,在Claude Desktop和Cursor中集成得较好。在更广泛的Agent开发框架(如LangChain)中,对MCP的完全支持还在演进中。但它的理念非常先进——解耦能力提供方和能力消费方。作为开发者,我们可以开始关注两方面:
- 作为消费者:在架构设计时,考虑未来通过MCP来集成工具,而不仅仅是硬编码API调用。
- 作为提供者:思考如何将你的Agent核心能力模块化,未来或许可以打包成一个MCP服务器,提供更广泛的服务。
7. 构建Skill工作流:从单点智能到流程自动化
单一的问答或工具调用是“点”,而Skill工作流是将这些“点”连成“线”,解决复杂任务。
7.1 工作流设计模式
- 顺序流:最直接的模式,A做完做B,B做完做C。适用于步骤明确、依赖清晰的任务,如“数据获取 -> 数据清洗 -> 数据分析 -> 生成报告”。
- 条件分支流:根据上一步的结果决定下一步的走向。这需要LLM或一个规则引擎来判断。例如,“查询天气”后,如果是“晴天”,则推荐“户外活动”;如果是“雨天”,则推荐“室内展览”。
- 并行与聚合流:多个可以独立执行的任务同时进行,最后汇总结果。例如,规划旅行时,同时查询“航班”、“酒店”、“景点门票”,等所有结果返回后,再统一生成方案。这能显著降低整体耗时。
- 循环与迭代流:用于需要反复调整或优化的任务。例如,生成一个图表,如果不满意,可以循环执行“分析问题 -> 调整参数 -> 重新生成”的步骤,直到满足条件或达到最大迭代次数。
7.2 工作流中的状态管理与错误处理
这是工作流稳定性的关键。
- 状态持久化:复杂工作流可能耗时很长(几分钟甚至几小时)。必须将每个步骤的输入、输出、执行状态(成功、失败、进行中)持久化到数据库。这样即使进程中断,重启后也能从断点恢复。
- 统一的错误处理框架:为工作流定义一个标准的错误类型和恢复策略。例如:
- 工具调用超时:重试3次。
- LLM输出格式错误:尝试修复,或回退到默认流程。
- 业务逻辑错误(如“库存不足”):触发一个“人工审核”节点,或向用户发送通知。
- 用户介入点:在关键决策点(如确认订单、选择最终方案)或工作流卡住时,设计优雅的“中断”机制,允许用户提供额外输入或做出选择,然后工作流继续。
7.3 编排工具的选择:LangGraph vs 其他
目前,LangGraph因其对LangChain生态的良好集成和清晰的状态图(StateGraph)模型,成为构建复杂Agent工作流的热门选择。它用“节点”和“边”来直观地定义工作流,非常适合实现上述各种模式。
当然,你也可以用更通用的工作流引擎(如Airflow、Prefect)或甚至自己用状态机来实现。核心在于,将Agent的“思考”和“行动”逻辑,从杂乱的if-else代码中抽离出来,变成一个可视、可管理、可监控的流程。
8. 测试、评估与监控:让Agent在线上稳定奔跑
开发完成只是第一步,让Agent在真实环境中可靠运行,需要一套完整的运维体系。
8.1 测试:模拟用户,暴露问题
- 单元测试:测试单个工具、单个Skill的功能是否正确。Mock掉LLM和外部API,保证逻辑无误。
- 集成测试:测试多个组件串联起来的工作流。可以使用一个轻量级的、确定性的LLM Mock(比如总是返回固定JSON)来验证流程编排。
- 端到端(E2E)测试:这是最重要的。准备一个涵盖核心场景的测试用例库(例如100个典型用户问题),用真实或接近真实的LLM(如GPT-3.5-Turbo)在全链路环境中跑。评估指标包括:
- 任务完成率:Agent是否最终给出了有效答案或完成了操作?
- 工具调用准确率:调用的工具和参数是否正确?
- 人工评分:随机抽取结果,让人工从“有用性”、“准确性”、“流畅性”维度评分。
8.2 评估:量化Agent的表现
除了测试时的指标,线上运行也需要持续评估。
- 业务指标:如果Agent用于客服,关注“问题解决率”、“转人工率”、“平均会话轮数”。如果用于销售,关注“线索转化率”。
- 成本指标:密切监控Token消耗、API调用次数和费用。分析哪些场景或用户消耗了主要成本,是否存在优化空间。
- 用户体验指标:通过用户反馈(点赞/点踩)、会话时长、退出率等间接评估体验。
8.3 监控与可观测性:快速发现并定位问题
Agent系统是动态的,随时可能因为外部API变化、模型波动或意料之外的用户输入而出错。
- 链路追踪:为每个用户会话分配唯一ID,记录下完整的执行链路:用户输入 -> LLM思考 -> 工具调用(及参数/结果)-> 最终输出。这就像飞机的“黑匣子”,当出现问题(比如用户投诉回答不对)时,可以快速回溯定位是哪个环节出了问题。
- 关键日志与告警:记录所有LLM的输入输出(可脱敏)、工具调用错误、耗时过长的操作。设置告警规则,例如:连续出现5次工具调用失败、或LLM返回格式错误的频率超过1%。
- “熔断”与降级:当检测到某个外部服务(如某个关键API)持续失败时,触发“熔断”机制,暂时停止调用该服务,并降级到备用方案或给用户一个友好的提示,防止故障扩散。
Agent开发,从技术炫技到工程落地,是一条从“知道所有零件”到“造出一辆能跑且安全的车”的道路。它要求我们不仅理解LLM、Memory、Tool这些组件的原理,更要以系统性的思维去设计它们之间的交互,以产品化的思维去关注稳定性、成本和用户体验。这个过程充满挑战,但每当看到一个自己打造的Agent,能稳定、流畅地帮用户解决一个真实问题时,那种成就感,远非跑通一个Demo可比。这条路还很长,但方向已经越来越清晰。
