Agent 不是魔法按钮:我最近重新理解的一套工程知识
最近做项目的时候,我对 Agent 的理解有一点变化。
一开始很容易把 Agent 想成“一个更聪明的大模型”:用户说一句话,模型理解、思考、调用工具,然后优雅地给出结果。但真正落到工程里,会发现 Agent 最重要的不是“聪明”,而是“可控”。
如果一个 Agent 只能在演示里跑通一次,却不能记录过程、不能失败重试、不能解释为什么这么做、不能把结果稳定落库,那它就很难变成一个能长期运行的系统。
所以我现在更愿意这样理解 Agent:
1 | Agent = 大模型推理能力 + 工作流编排 + 工具调用 + 记忆 + 校验 + 可追踪日志 |
大模型只是其中一环。真正让 Agent 从 Demo 变成产品的,是它周围那一整套工程体系。
1. Agent 不应该直接暴露给前端
我现在比较认同的一种架构是:Agent 服务独立存在,但不直接暴露给前端。
更合理的链路是:
1 | Client |
前端只和业务后端通信。后端负责鉴权、参数校验、用户上下文组装、结果保存,Agent 负责理解需求、检索知识、生成候选方案和解释原因。
这样做的好处很明显:
- 前端不会暴露内部 Agent 地址。
- 模型密钥不会出现在客户端。
- 用户权限和业务数据仍然由后端控制。
- Agent 服务可以独立部署和迭代。
- Agent 出问题时,不会直接把主业务服务拖垮。
这也是我现在看 Agent 架构时的一个基本判断:Agent 可以很智能,但它不应该越过业务后端成为公开入口。
2. 结构化输入,比“把用户原话丢给模型”靠谱
以前写 Prompt 时,很容易把用户原话和系统上下文拼成一大段文本。
后来发现,这样做短期方便,长期很难维护。因为你很难知道模型到底看到了哪些字段,也很难在日志里复现问题。
更好的方式是先把请求结构化:
1 | { |
自然语言适合表达模糊意图,但工程系统需要字段。
结构化输入的价值是:
- 能校验。
- 能记录。
- 能复现。
- 能扩展。
- 能区分用户原话和系统上下文。
如果一个 Agent 的输入就是一段不可控的长 Prompt,后面出问题时基本只能靠猜。
3. 输出也要结构化,否则后端很难接住
Agent 的输出也不能只是一段“看起来挺对”的文本。
如果后端要继续保存、展示、计算、校验,那输出必须稳定。
我比较喜欢这种返回结构:
1 | { |
这里面几个字段都很重要:
request_id用来串日志。status告诉后端到底成功还是失败。result是最终结果。retrieval记录召回了哪些信息。validation说明结果有没有通过校验。warnings告诉用户有什么边界情况。
我现在会比较警惕那种“模型输出一段自然语言,然后前端直接展示”的 Agent。它当然能跑,但很难进入严肃的业务流程。
4. Agent 不是一个 Prompt,而是一条 Workflow
如果任务稍微复杂一点,就不要试图用一个超级 Prompt 解决所有问题。
我更倾向于把 Agent 拆成一条工作流:
1 | Load Context |
每个节点只做一件事。
这样做有几个好处:
- 哪一步失败很清楚。
- 中间结果可以保存。
- 某个节点可以单独替换。
- 校验失败后可以回到生成节点重试。
- 后续可以加入人工确认。
这也是 LangGraph 这类框架有价值的地方。它不是让 Agent “更神秘”,而是让 Agent 的状态和步骤更清楚。
不过也不是所有项目一上来都要用 LangGraph。如果流程很简单,普通函数也够用。等到出现分支、重试、人工确认、多轮工具调用时,再上状态图会更自然。
5. State 是 Agent 的骨架
一个多步骤 Agent 最怕的是中间状态都藏在 Prompt 里。
更好的方式是显式定义 State:
1 | class AgentState(TypedDict): |
每个节点读一部分 State,再写入一部分 State。
比如:
- Parse Intent 写入
parsed_intent - Retrieve Knowledge 写入
retrieved_documents - Generate Candidates 读取上下文并写入候选结果
- Validate 写入
validation_result - Compose Response 输出最终结果
这样整个 Agent 就不再是一团大 Prompt,而是一条可观察、可调试的状态流。
6. RAG 的关键不是“有向量库”,而是怎么用
现在一说 RAG,大家很容易直接想到“接个向量数据库”。
但我感觉 RAG 真正重要的是边界:
1 | 向量库负责召回,关系型数据库负责事实。 |
向量库适合放语义描述,比如:
- 内容摘要
- 场景知识
- 用户偏好摘要
- 历史行为摘要
- 可召回对象描述
关系型数据库适合放事实,比如:
- 用户
- 权限
- 状态
- 价格
- 订单
- 业务主表
向量库的 payload 里可以保存主表 ID,但不要把向量库当主库。召回之后最好回表查一遍,再做过滤和校验。
比较稳的 RAG 流程是:
1 | Build Semantic Query |
向量召回只是候选,不是最终答案。
7. Semantic Query 不要只拿用户原话
用户原话通常太短,也不一定包含完整上下文。
比如用户说:“想吃点清淡的。”
这句话本身信息很少。如果直接拿去 embedding,召回效果可能很一般。
更好的做法是把上下文一起拼进去:
1 | def build_semantic_query(request): |
也就是说,Semantic Query 本身就是一个小型的信息融合过程。
它不只是用户说了什么,还包括:
- 用户长期偏好
- 最近反馈
- 当前目标
- 业务约束
- 历史结果
- 同义词扩展
- 场景标签
RAG 好不好,很多时候不只取决于向量库,也取决于查询文本怎么构造。
8. Memory 不是聊天记录,而是可召回的偏好摘要
我以前会自然地把“记忆”理解成聊天记录。
但做 Agent 时会发现,原始聊天记录不一定是最好的记忆形态。它太长、太碎,也不一定适合召回。
更好用的是总结后的偏好记忆。
比如:
1 | 用户偏好:喜欢清淡、简单、低成本方案。不喜欢太复杂、耗时长、油腻的内容。 |
这类记忆可以来自:
- 收藏
- 点赞
- 差评
- 历史选择
- 用户主动填写
- 最近生成记录
原始事件仍然放在 MySQL 里,语义摘要放进向量库里。下次生成时,再把相关记忆召回出来。
这比“把所有历史对话塞进 Prompt”更干净。
9. 反馈应该反哺下一次生成
如果一个 Agent 每次都像第一次见用户,那它就不够“智能”。
一个很实用的闭环是:
1 | 用户反馈 |
反馈不一定全部交给 LLM 处理,也可以参与系统打分。
例如:
1 | def feedback_score(item, feedback_memory): |
这类规则不复杂,但很有效。模型负责理解和生成,系统负责把用户反馈稳定地沉淀下来。
10. 关键计算不要交给 LLM
这是我觉得特别重要的一点。
LLM 可以生成候选,可以解释原因,可以总结偏好,但不要让它负责精确计算。
这些事情更适合确定性工具:
- 价格
- 数量
- 权限
- 库存
- 营养
- 时间
- 风险等级
- 业务规则校验
- 数据库查询
- 排序打分
正确分工应该是:
1 | LLM:理解需求、生成候选、解释原因 |
例如:
1 | candidate = llm_generate_candidate(context) |
这个思路能减少很多幻觉。不要让模型“看起来算对了”,要让工具真的算对。
11. 生成后必须校验
Agent 生成结果后,不能直接返回。
至少要做几类校验:
- JSON schema 是否正确
- 必填字段是否完整
- 业务约束是否满足
- 数量、预算、范围是否越界
- 是否包含被排除项
- 是否和历史结果过度重复
- 是否需要返回 warning
- 是否可以重试生成
比较稳的流程:
1 | Generate Candidate |
失败时不要硬编一个成功结果。明确失败原因比“看似成功但实际不可用”更重要。
12. Agent 不一定只靠模型选结果
很多时候,候选结果可以通过打分排序,而不是完全交给模型判断。
打分可以来自:
- 语义召回分数
- 用户偏好分数
- 历史重复惩罚
- 反馈记忆分数
- 约束匹配分数
- 成本分数
- 风险分数
- 业务优先级
示例:
1 | final_score = ( |
这种“模型生成 + 系统打分”的方式,比完全让模型拍脑袋选结果更稳定。
13. request_id 是 Agent 系统里的救命绳
Agent 调用一定要可追踪。
每次运行最好记录:
request_iduser_idworkflow_versionprompt_versionmodel_nameinput_jsonoutput_jsonretrieval_resultvalidation_resulterror_messagestarted_atfinished_at
可以有一张 agent_runs 表:
1 | agent_runs |
没有这些日志,Agent 出问题时很痛苦。你不知道它当时召回了什么,不知道 Prompt 是哪个版本,也不知道模型输出了什么。
有日志之后,才能复现、回放、对比和优化。
14. Prompt 也要版本管理
Prompt 不是临时文案,它更像代码。
建议记录:
prompt_versionworkflow_versionmodel_nameschema_version- 关键参数
因为同一个输入,在不同 Prompt 版本下可能输出完全不一样。
如果线上效果突然变差,没有版本记录就很难判断:
- 是模型变了?
- 是 Prompt 变了?
- 是知识库变了?
- 是用户输入变了?
- 是校验规则变了?
所以 Prompt 要像接口一样管理版本。
15. LangGraph 什么时候有价值?
不是所有 Agent 都必须用 LangGraph。
如果流程只是:
1 | parse -> retrieve -> generate -> validate |
普通函数就够了。
但当流程出现这些情况时,LangGraph 会更有价值:
- 多个可复用节点
- 多条件分支
- 失败重试
- 人工确认
- 多轮工具调用
- 需要中断和恢复
- 需要可视化工作流
- 需要清晰管理状态流转
我理解 LangGraph 的价值不是“让 Agent 更像 Agent”,而是让复杂 Agent 的状态流更工程化。
16. Agent 应用里我最容易踩的坑
1. 让 LLM 直接做计算
看起来省事,实际上容易出错。
2. 没有结构化输出
模型返回一段漂亮文字,后端却接不住。
3. 没有失败分支
只考虑成功路径,一到线上就很被动。
4. 没有记忆机制
每次生成都像第一次认识用户。
5. 向量库保存事实
时间久了很容易和主数据库不一致。
6. 前端直接调用 Agent
内部服务、模型接口和权限边界都会变乱。
7. 没有 request_id
日志串不起来,问题很难查。
8. Prompt 没有版本
效果变化后很难回滚和定位。
17. 一个我觉得比较健康的 Agent 最小闭环
现在我会把一个可落地 Agent 的最小闭环理解成这样:
1 | 结构化请求 |
这个闭环里,大模型不是全部,它只是其中一个推理节点。
真正让 Agent 可用的,是:
- 结构化输入输出
- 清晰 Workflow
- 可观察 State
- RAG 召回
- 长期 Memory
- Deterministic Tools
- Validation
- Trace
- 后端编排
- 独立部署
总结
我现在越来越觉得,Agent 不是魔法按钮。
它不是“用户说一句话,大模型自动搞定一切”。如果要做成真正可用的应用,Agent 反而需要很多非常朴素的工程能力:字段、状态、日志、校验、工具、数据库、版本、部署。
大模型负责理解和生成,工程系统负责约束、计算、校验和追踪。
两者结合起来,Agent 才能从一个演示变成一个可靠的系统。