最近做项目的时候,我对 Agent 的理解有一点变化。

一开始很容易把 Agent 想成“一个更聪明的大模型”:用户说一句话,模型理解、思考、调用工具,然后优雅地给出结果。但真正落到工程里,会发现 Agent 最重要的不是“聪明”,而是“可控”。

如果一个 Agent 只能在演示里跑通一次,却不能记录过程、不能失败重试、不能解释为什么这么做、不能把结果稳定落库,那它就很难变成一个能长期运行的系统。

所以我现在更愿意这样理解 Agent:

1
Agent = 大模型推理能力 + 工作流编排 + 工具调用 + 记忆 + 校验 + 可追踪日志

大模型只是其中一环。真正让 Agent 从 Demo 变成产品的,是它周围那一整套工程体系。

1. Agent 不应该直接暴露给前端

我现在比较认同的一种架构是:Agent 服务独立存在,但不直接暴露给前端。

更合理的链路是:

1
2
3
4
Client
-> Backend API
-> Agent Service
-> Database / Vector DB / Tools

前端只和业务后端通信。后端负责鉴权、参数校验、用户上下文组装、结果保存,Agent 负责理解需求、检索知识、生成候选方案和解释原因。

这样做的好处很明显:

  • 前端不会暴露内部 Agent 地址。
  • 模型密钥不会出现在客户端。
  • 用户权限和业务数据仍然由后端控制。
  • Agent 服务可以独立部署和迭代。
  • Agent 出问题时,不会直接把主业务服务拖垮。

这也是我现在看 Agent 架构时的一个基本判断:Agent 可以很智能,但它不应该越过业务后端成为公开入口。

2. 结构化输入,比“把用户原话丢给模型”靠谱

以前写 Prompt 时,很容易把用户原话和系统上下文拼成一大段文本。

后来发现,这样做短期方便,长期很难维护。因为你很难知道模型到底看到了哪些字段,也很难在日志里复现问题。

更好的方式是先把请求结构化:

1
2
3
4
5
6
7
8
9
10
{
"request_id": "uuid",
"user_id": "10001",
"scene": "generate_plan",
"goal": "balanced",
"user_text": "想要清淡一点,别太复杂",
"preference_context": {},
"history_context": {},
"constraints": {}
}

自然语言适合表达模糊意图,但工程系统需要字段。

结构化输入的价值是:

  • 能校验。
  • 能记录。
  • 能复现。
  • 能扩展。
  • 能区分用户原话和系统上下文。

如果一个 Agent 的输入就是一段不可控的长 Prompt,后面出问题时基本只能靠猜。

3. 输出也要结构化,否则后端很难接住

Agent 的输出也不能只是一段“看起来挺对”的文本。

如果后端要继续保存、展示、计算、校验,那输出必须稳定。

我比较喜欢这种返回结构:

1
2
3
4
5
6
7
8
9
10
{
"request_id": "uuid",
"status": "succeeded",
"result": {},
"retrieval": {},
"validation": {
"passed": true,
"warnings": []
}
}

这里面几个字段都很重要:

  • request_id 用来串日志。
  • status 告诉后端到底成功还是失败。
  • result 是最终结果。
  • retrieval 记录召回了哪些信息。
  • validation 说明结果有没有通过校验。
  • warnings 告诉用户有什么边界情况。

我现在会比较警惕那种“模型输出一段自然语言,然后前端直接展示”的 Agent。它当然能跑,但很难进入严肃的业务流程。

4. Agent 不是一个 Prompt,而是一条 Workflow

如果任务稍微复杂一点,就不要试图用一个超级 Prompt 解决所有问题。

我更倾向于把 Agent 拆成一条工作流:

1
2
3
4
5
6
7
8
Load Context
-> Parse Intent
-> Build Retrieval Query
-> Retrieve Knowledge
-> Generate Candidates
-> Run Tools
-> Validate
-> Compose Response

每个节点只做一件事。

这样做有几个好处:

  • 哪一步失败很清楚。
  • 中间结果可以保存。
  • 某个节点可以单独替换。
  • 校验失败后可以回到生成节点重试。
  • 后续可以加入人工确认。

这也是 LangGraph 这类框架有价值的地方。它不是让 Agent “更神秘”,而是让 Agent 的状态和步骤更清楚。

不过也不是所有项目一上来都要用 LangGraph。如果流程很简单,普通函数也够用。等到出现分支、重试、人工确认、多轮工具调用时,再上状态图会更自然。

5. State 是 Agent 的骨架

一个多步骤 Agent 最怕的是中间状态都藏在 Prompt 里。

更好的方式是显式定义 State:

1
2
3
4
5
6
7
8
9
10
11
12
13
class AgentState(TypedDict):
request: dict
user_context: dict
parsed_intent: dict
retrieval_plan: dict
retrieved_documents: list[dict]
user_memories: list[dict]
candidate_results: list[dict]
selected_result: dict
calculated_metrics: dict
validation_result: dict
final_response: dict
errors: list[dict]

每个节点读一部分 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
2
3
4
5
6
7
Build Semantic Query
-> Vector Search
-> Get IDs
-> Query MySQL
-> Filter
-> Score
-> Generate

向量召回只是候选,不是最终答案。

7. Semantic Query 不要只拿用户原话

用户原话通常太短,也不一定包含完整上下文。

比如用户说:“想吃点清淡的。”

这句话本身信息很少。如果直接拿去 embedding,召回效果可能很一般。

更好的做法是把上下文一起拼进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def build_semantic_query(request):
parts = [
request.user_text,
request.goal,
request.scene,
]

if request.preference_context:
parts.append(build_preference_text(request.preference_context))

if request.history_context:
parts.append(build_history_text(request.history_context))

if request.constraints:
parts.append(build_constraint_text(request.constraints))

return " ".join(parts)

也就是说,Semantic Query 本身就是一个小型的信息融合过程。

它不只是用户说了什么,还包括:

  • 用户长期偏好
  • 最近反馈
  • 当前目标
  • 业务约束
  • 历史结果
  • 同义词扩展
  • 场景标签

RAG 好不好,很多时候不只取决于向量库,也取决于查询文本怎么构造。

8. Memory 不是聊天记录,而是可召回的偏好摘要

我以前会自然地把“记忆”理解成聊天记录。

但做 Agent 时会发现,原始聊天记录不一定是最好的记忆形态。它太长、太碎,也不一定适合召回。

更好用的是总结后的偏好记忆。

比如:

1
用户偏好:喜欢清淡、简单、低成本方案。不喜欢太复杂、耗时长、油腻的内容。

这类记忆可以来自:

  • 收藏
  • 点赞
  • 差评
  • 历史选择
  • 用户主动填写
  • 最近生成记录

原始事件仍然放在 MySQL 里,语义摘要放进向量库里。下次生成时,再把相关记忆召回出来。

这比“把所有历史对话塞进 Prompt”更干净。

9. 反馈应该反哺下一次生成

如果一个 Agent 每次都像第一次见用户,那它就不够“智能”。

一个很实用的闭环是:

1
2
3
4
5
6
用户反馈
-> 后端保存原始反馈
-> 构造反馈记忆文本
-> 写入向量库
-> 下一次生成时召回
-> 影响候选排序或生成约束

反馈不一定全部交给 LLM 处理,也可以参与系统打分。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def feedback_score(item, feedback_memory):
score = 0

if item.name in feedback_memory.liked_names:
score += 10

if item.name in feedback_memory.disliked_names:
score -= 20

if item.tags & feedback_memory.boost_tags:
score += 5

if item.tags & feedback_memory.avoid_tags:
score -= 12

return score

这类规则不复杂,但很有效。模型负责理解和生成,系统负责把用户反馈稳定地沉淀下来。

10. 关键计算不要交给 LLM

这是我觉得特别重要的一点。

LLM 可以生成候选,可以解释原因,可以总结偏好,但不要让它负责精确计算。

这些事情更适合确定性工具:

  • 价格
  • 数量
  • 权限
  • 库存
  • 营养
  • 时间
  • 风险等级
  • 业务规则校验
  • 数据库查询
  • 排序打分

正确分工应该是:

1
2
LLM:理解需求、生成候选、解释原因
Tool:计算、查询、校验、排序、落库

例如:

1
2
3
4
5
6
candidate = llm_generate_candidate(context)
metrics = calculate_metrics(candidate)
validation = validate(candidate, metrics)

if not validation.passed:
retry_or_return_failure()

这个思路能减少很多幻觉。不要让模型“看起来算对了”,要让工具真的算对。

11. 生成后必须校验

Agent 生成结果后,不能直接返回。

至少要做几类校验:

  • JSON schema 是否正确
  • 必填字段是否完整
  • 业务约束是否满足
  • 数量、预算、范围是否越界
  • 是否包含被排除项
  • 是否和历史结果过度重复
  • 是否需要返回 warning
  • 是否可以重试生成

比较稳的流程:

1
2
3
4
5
6
Generate Candidate
-> Calculate
-> Validate
-> Pass: Compose Response
-> Fail: Retry
-> Retry Exhausted: Return Failure

失败时不要硬编一个成功结果。明确失败原因比“看似成功但实际不可用”更重要。

12. Agent 不一定只靠模型选结果

很多时候,候选结果可以通过打分排序,而不是完全交给模型判断。

打分可以来自:

  • 语义召回分数
  • 用户偏好分数
  • 历史重复惩罚
  • 反馈记忆分数
  • 约束匹配分数
  • 成本分数
  • 风险分数
  • 业务优先级

示例:

1
2
3
4
5
6
7
final_score = (
semantic_score
+ preference_score
+ feedback_score
+ history_penalty
+ constraint_score
)

这种“模型生成 + 系统打分”的方式,比完全让模型拍脑袋选结果更稳定。

13. request_id 是 Agent 系统里的救命绳

Agent 调用一定要可追踪。

每次运行最好记录:

  • request_id
  • user_id
  • workflow_version
  • prompt_version
  • model_name
  • input_json
  • output_json
  • retrieval_result
  • validation_result
  • error_message
  • started_at
  • finished_at

可以有一张 agent_runs 表:

1
2
3
4
5
6
7
8
9
10
11
12
13
agent_runs
id
request_id
user_id
status
input_json
output_json
error_message
model_name
prompt_version
workflow_version
started_at
finished_at

没有这些日志,Agent 出问题时很痛苦。你不知道它当时召回了什么,不知道 Prompt 是哪个版本,也不知道模型输出了什么。

有日志之后,才能复现、回放、对比和优化。

14. Prompt 也要版本管理

Prompt 不是临时文案,它更像代码。

建议记录:

  • prompt_version
  • workflow_version
  • model_name
  • schema_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
2
3
4
5
6
7
8
9
10
11
12
结构化请求
-> 加载用户上下文
-> 构造语义查询
-> RAG 召回候选
-> 回表校验事实
-> 生成候选结果
-> 确定性工具计算
-> 业务规则校验
-> 保存 agent_run 日志
-> 保存最终结果
-> 同步长期记忆
-> 返回结构化响应

这个闭环里,大模型不是全部,它只是其中一个推理节点。

真正让 Agent 可用的,是:

  • 结构化输入输出
  • 清晰 Workflow
  • 可观察 State
  • RAG 召回
  • 长期 Memory
  • Deterministic Tools
  • Validation
  • Trace
  • 后端编排
  • 独立部署

总结

我现在越来越觉得,Agent 不是魔法按钮。

它不是“用户说一句话,大模型自动搞定一切”。如果要做成真正可用的应用,Agent 反而需要很多非常朴素的工程能力:字段、状态、日志、校验、工具、数据库、版本、部署。

大模型负责理解和生成,工程系统负责约束、计算、校验和追踪。

两者结合起来,Agent 才能从一个演示变成一个可靠的系统。