一、从 Workflow 到 Agentic Workflow
1. 为什么不能只靠纯 Agent?
很多人刚接触 Agent 时,会觉得:
既然大模型可以推理、规划、调用工具,那是不是把所有事情交给 Agent 就行了?
实际工程里并不是这样。
纯 Agent 最大的问题是不可控。
尤其是 ReAct 这类模式:
text
Thought -> Action -> Observation -> Thought -> Action -> Observation ...
如果缺少外部控制,很容易出现几个问题。
2. 痛点一:Agent 陷入死循环
比如 Agent 调用了一个错误工具,工具返回失败,Agent 又继续调用同一个工具。
或者 Agent 对原始目标理解偏了,后面每一步都在错误方向上反复修正。
结果就是:
text
重复调用同一个工具
重复生成无效方案
Token 持续消耗
任务最终失败
所以工程上不能让 Agent 无限循环,需要加控制:
text
最大迭代次数
状态机约束
失败兜底策略
高风险操作人工确认
一句话:
Agent 可以自主决策,但不能无限自由发挥。
3. 痛点二:工具调用失败率高
如果给 Agent 挂很多工具,比如 20 个、30 个,它不一定能稳定选对。
常见问题包括:
text
工具选错
参数格式错
JSON Schema 不符合要求
工具调用顺序混乱
工具返回结果无法正确解释
所以工具不能简单堆给一个"超级大脑"。
更合理的做法是:
text
工具分层
工具路由
Schema 校验
Negative Few-shot 反例提示
Multi-Agent 分工
比如拆成:
text
检索 Agent
分析 Agent
执行 Agent
审核 Agent
不同 Agent 只负责自己擅长的一类任务。
4. Agentic Workflow 是什么?
Agentic Workflow 并不是单纯的 Workflow,也不是完全自治的 Agent,而是在工程实践中将二者结合起来的一种架构模式:
text
固定流程 + 局部智能体
也就是说,外层用 Workflow 控制主流程,保证稳定性;内层在复杂节点引入 Agent,提升处理上限。
比如客服场景:
text
节点 1:解析客户邮件
↓
节点 2:Agent 介入
- 查知识库
- 判断情绪
- 生成回复
- 自我检查
↓
节点 3:合规审核并发送
这里的重点是:
| 组件 | 作用 |
|---|---|
| Workflow | 控制大方向,保证流程稳定 |
| Agent | 处理复杂、不确定、需要推理的节点 |
| 状态管理 | 防止流程乱跳和死循环 |
| 工具治理 | 防止工具误用 |
| 人工介入 | 控制高风险操作 |
所以 Agentic Workflow 的核心不是"让 Agent 接管一切",而是:
在规定好的流程节点里,让 Agent 做需要智能判断的事情。
5. 小结
这一部分可以归纳为:
Agentic Workflow 不是把 Workflow 和 Agent 对立起来,而是把二者融合。Workflow 负责确定性流程编排,保证任务边界、状态流转和失败兜底;Agent 负责非确定性节点,比如意图识别、知识检索、方案生成、多轮反思和工具调用。
在工程落地时,不能只靠 Prompt 驱动 Agent,因为纯 Agent 容易出现死循环、工具幻觉、状态失控和输出不可控。所以需要引入状态机、最大迭代次数、工具分层、Schema 校验、人工确认以及审计日志。
最终目标是用 Workflow 稳定系统下限,用 Agent 提升复杂任务处理上限。
核心结论:
text
Workflow 稳定下限,Agent 提升上限。
二、大模型量化的核心原理
1. 量化到底是什么?
模型量化不是减少模型层数,也不是删除参数,而是:
把原来用 FP16 / FP32 表示的高精度数字,映射成 INT8 / INT4 等低精度整数,从而减少显存占用、降低带宽压力、提升推理速度。
比如:
text
FP16:每个参数 16 bit
INT4:每个参数 4 bit
理论上权重存储可以减少 4 倍。
所以量化解决的是:
text
显存更省
带宽更低
推理更快
部署更容易
代价是:
text
可能损失精度
需要处理离群值
需要适配硬件和推理框架
2. 量化的数学公式
量化最核心的公式是:
text
Real = Scale × (Quantized - ZeroPoint)
反过来:
text
Quantized = round(Real / Scale + ZeroPoint)
其中:
| 名称 | 含义 |
|---|---|
| Real | 原始浮点数 |
| Quantized | 量化后的整数 |
| Scale | 缩放比例 |
| ZeroPoint | 零点偏移 |
举个例子,一组权重范围是:
text
-1.0 ~ 1.0
如果用 INT8 表示:
text
-128 ~ 127
那么可以粗略理解为:
text
一个整数单位 ≈ 0.0078
于是:
text
真实值 0.78 ≈ 整数 100
真实值 -0.39 ≈ 整数 -50
所以量化过程可以概括为:
text
浮点数 -> 整数 -> 近似浮点数
这个过程会有误差,但只要误差可控,模型能力就不会明显下降。
3. 对称量化与非对称量化
对称量化
text
ZeroPoint = 0
Real = Scale × Quantized
优点:
text
计算简单
硬件友好
缺点:
text
如果数据分布不对称,会浪费整数区间
非对称量化
text
ZeroPoint != 0
Real = Scale × (Quantized - ZeroPoint)
适合真实值范围不对称的场景,比如:
text
0 ~ 6
这种情况下,如果还用对称量化,负数区间就浪费了。
4. Per-Tensor、Per-Channel、Per-Group
量化还有一个很关键的概念:量化粒度。
Per-Tensor
整个权重矩阵共用一套:
text
Scale + ZeroPoint
优点是快,缺点是精度损失大。
Per-Channel
每一行或者每一列单独一套:
text
Scale + ZeroPoint
它是精度和性能之间的折中。
Per-Group
把一行或一列再切成小组,比如:
text
每 128 个元素一组
每组一套量化参数。
大模型里很常见:
text
group_size = 128
它比 Per-Tensor 更细,比每个元素单独量化更省。
5. 为什么大模型量化更难?
关键是:
text
Activation Outliers,激活值离群点
大模型里有些激活值会特别大。
比如大多数值在:
text
-1 ~ 1
但是少数值可能是:
text
50、100、200
如果直接全局量化,为了容纳这些极端大值,Scale 必须变大。
结果就是:
text
Scale 变大 -> 普通小值被压到 0 或 1 附近
这会导致大量正常特征精度丢失,模型能力崩掉。
所以大模型量化的核心难点不是"会不会把 float 转 int",而是:
如何在离群值存在的情况下,仍然保住大部分正常特征的表达精度。
6. W4A16 和 W8A8
Weight-Only:W4A16
text
Weight 用 INT4
Activation 仍然用 FP16
它主要解决:
text
显存占用
显存带宽
典型方案:
text
GPTQ
AWQ
GGUF / llama.cpp 里的 Q4、Q5、Q8
Weight + Activation:W8A8
text
Weight 用 INT8
Activation 也用 INT8
它主要解决:
text
计算瓶颈
但是难度更高,因为 Activation 是运行时动态产生的,而且离群值更多。
所以 W8A8 通常需要:
text
SmoothQuant
混合精度
离群通道单独计算
7. GPTQ、AWQ、SmoothQuant
GPTQ
GPTQ 不是简单四舍五入,而是利用二阶信息估计量化误差对输出的影响,再做补偿。
可以概括为:
GPTQ 会利用 Hessian(二阶信息)估计不同权重对输出误差的影响,并优先补偿重要权重,从而降低整体量化误差。
AWQ
AWQ 的思想是:
text
不是所有权重都同等重要
它会根据激活分布找出重要通道,对重要通道做保护。
可以概括为:
AWQ 会根据 Activation 分布识别重要通道,并优先保护这些通道的量化精度,从而降低 Weight-Only Quantization 带来的精度损失。
SmoothQuant
SmoothQuant 主要解决 W8A8 里的激活离群点问题。
它的思路是:
text
激活难量化,权重好量化;
那就把激活侧的尖峰平滑掉,
同时对权重做反向补偿。
也就是:
SmoothQuant 会将部分 Activation 的量化难度转移到 Weight,通过对 Activation 和 Weight 同时进行缩放变换,降低激活离群值带来的影响。
8. 小结
这一部分可以归纳为:
模型量化的核心原理,是把模型中的高精度浮点数,例如 FP16 或 FP32,通过 Scale 和 ZeroPoint 映射到低精度整数,例如 INT8 或 INT4。推理时再根据
Real = Scale × (Quantized - ZeroPoint)近似还原真实数值,从而减少显存占用和访存带宽,并在硬件支持下提升矩阵乘法速度。工程上量化粒度可以分为 Per-Tensor、Per-Channel 和 Per-Group。Per-Tensor 最高效但精度损失大,Per-Channel 和 Per-Group 会为不同通道或分组设置独立量化参数,能更好适配大模型权重分布。
大模型量化的难点主要在激活值离群点。少量极大的 Activation Outliers 会拉大量化范围,导致大部分正常值被压缩到 0 或 1 附近,从而造成精度崩溃。因此主流方案会分成两类:一类是 Weight-Only,例如 W4A16,主要解决显存和带宽瓶颈;另一类是 Weight + Activation,例如 W8A8,主要解决计算瓶颈,但需要 SmoothQuant、混合精度或者离群通道处理来保证精度。
三、为什么 Transformer 不能简单去掉多头?
1. 多头注意力解决了什么?
多头注意力不是简单地"多算几遍注意力",而是让模型在不同子空间里分别学习不同关系。
比如一句话:
text
小明把苹果放进书包,因为他明天要去学校。
不同头可能关注不同信息:
| 注意力头 | 可能关注的信息 |
|---|---|
| 头 1 | 主谓宾关系 |
| 头 2 | 指代关系 |
| 头 3 | 位置关系 |
| 头 4 | 长距离依赖 |
| 头 5 | 复制、归纳、模式匹配 |
所以多头的价值是:
text
不同头负责不同视角的信息抽取。
2. 单头的问题一:特征子空间丧失
比如:
text
hidden_size = 4096
num_heads = 32
head_dim = 128
每个头都在自己的 128 维子空间里学习一种注意力模式。
如果变成单头:
text
hidden_size = 4096
num_heads = 1
head_dim = 4096
表面上维度更大了,但只有一套注意力分布。
问题不是维度不够,而是:
text
注意力模式数量不够。
3. 单头的问题二:注意力分布冲突
注意力计算大致是:
text
Attention(Q, K, V) = softmax(QK^T / sqrt(d)) V
关键在于:
text
softmax 只能产生一套概率分布。
如果一个 token 同时需要关注:
text
前面的主语
后面的宾语
长距离上下文
某个格式模板
某个指代对象
单头只能把所有需求挤在同一个 softmax 里。
结果就是:
text
注意力被平均
注意力被稀释
输出特征变模糊
这就是所谓的 over-smoothing。
4. 单头的问题三:Induction Head 能力变弱
大模型里有一种很重要的机制叫:
text
Induction Head
Induction Head 指模型能够识别上下文中已经出现的模式,并在后续位置复用这一模式继续推理或生成。
比如:
text
A -> 1
B -> 2
C -> ?
模型需要从上下文中归纳出:
text
C -> 3
这类能力通常依赖多个注意力头协作:
text
有的头负责找历史模式
有的头负责复制结构
有的头负责定位当前位置
有的头负责传递结果
如果只剩一个头,这些能力都会被削弱。
5. MHA、MQA、GQA、MLA
MHA:标准多头注意力
text
多个 Q
多个 K
多个 V
优点:
text
表达能力强
缺点:
text
KV Cache 显存占用大
MQA:Multi-Query Attention
text
多个 Q
一个 K
一个 V
注意,MQA 不是彻底单头。
它保留了多个 Query 头,只是让多个 Q 共享同一组 K/V。
优点:
text
KV Cache 大幅下降
缺点:
text
表达能力下降
GQA:Grouped-Query Attention
text
多个 Q
多组 K/V
比如:
text
32 个 Query 头
8 组 Key/Value
每 4 个 Q 共享一组 K/V。
它是 MHA 和 MQA 的折中,也是很多主流大模型采用的方案。
MLA:Latent Attention
MLA 可以理解成进一步压缩 KV,把 KV 压缩成低秩 latent 表示,在计算时再动态恢复。
目标是:
text
尽量保留表达能力
极大降低 KV Cache
6. 小结
这一部分可以归纳为:
如果 Transformer 去掉多头只用单头,核心问题不是参数量简单变少,而是注意力表达能力下降。多头注意力的本质是让模型在多个子空间中并行学习不同的注意力模式,比如语法关系、语义关系、位置关系、指代关系和长距离依赖。
如果只保留单头,模型只有一套 softmax 注意力分布,所有信息关系都被挤在同一个注意力分布里,容易出现注意力冲突和 over-smoothing,导致特征变模糊。这样会削弱模型对复杂上下文的建模能力,尤其会影响 induction head、in-context learning、复制模式和长上下文推理能力。
工程上之所以又出现 MQA、GQA、MLA,不是因为多头没用,而是因为标准 MHA 在长文本推理时 KV Cache 显存开销太大。MQA 让多个 Query 头共享一组 K/V,极大降低 KV Cache,但会牺牲一部分表达能力;GQA 则是在 MHA 和 MQA 之间折中;MLA 进一步把 KV 压缩成低秩 latent 表示,降低长上下文推理成本。
核心结论:
text
多头不是为了凑参数,而是为了学习多种注意力模式。
MQA/GQA/MLA 的目标不是消灭多头,而是降低 KV Cache。
四、后端 Agent 平台架构设计
1. 多模型支持怎么设计?
多模型支持不是在代码里写一堆:
text
if model == gpt
if model == claude
if model == qwen
而是要做成分层架构:
text
业务请求
↓
统一协议层
↓
模型适配器层
↓
智能路由层
↓
具体模型供应商
| 层级 | 作用 |
|---|---|
| 统一协议层 | 把不同模型抽象成统一请求/响应格式 |
| 模型适配器层 | 屏蔽 OpenAI、Claude、Qwen、DeepSeek 等接口差异 |
| 智能路由层 | 根据成本、延迟、能力、限流情况选择模型 |
| 降级备用链路 | 主模型异常时切到备用模型 |
可以抽象成:
kotlin
interface LLMProvider {
suspend fun chat(request: ChatRequest): ChatResponse
suspend fun streamChat(request: ChatRequest): Flow<ChatChunk>
}
然后实现:
text
OpenAIAdapter
ClaudeAdapter
QwenAdapter
DeepSeekAdapter
LocalModelAdapter
业务层不直接关心底层调用哪个模型。
2. 多租户模型切换能不能热更新?
工程实践中通常需要支持。
比如:
text
租户 A 从 GPT-4o 切到 DeepSeek
租户 B 从 Qwen 切到 Claude
这不应该重启服务。
工程上可以通过配置中心实现:
text
Apollo / Nacos / Consul / DB Config
配置示例:
json
{
"tenant_id": "tenant_a",
"primary_model": "gpt-4o",
"fallback_model": "deepseek-v3",
"temperature": 0.7,
"max_tokens": 4096,
"rate_limit": 1000
}
配置变更流程:
text
配置中心变更
↓
Agent Gateway 监听
↓
更新内存缓存
↓
新请求使用新模型
↓
旧请求保持原模型继续执行
关键点是:
正在执行中的请求不要强制切换,新请求走新配置。
这就是内存双缓冲切换。
3. 后端 Agent 是否支持多租户并发?
后端 Agent 平台通常需要支持多租户隔离与并发。
核心是三个词:
text
隔离
传播
限流
每个请求都应该带:
text
tenant_id
user_id
session_id
agent_id
trace_id
这些上下文要贯穿:
text
Agent 调用
模型调用
工具调用
记忆查询
SSE 推流
在 Java / Kotlin 里可以用:
text
ThreadLocal
CoroutineContext
MDC
RequestContext
封装成:
kotlin
data class AgentContext(
val tenantId: String,
val userId: String,
val sessionId: String,
val agentId: String,
val traceId: String
)
4. 租户级限流
不能只做全局限流,否则一个租户的突发流量会拖垮所有租户。
应该分层限流:
text
全局限流
租户级限流
用户级限流
模型级限流
工具级限流
常见实现:
text
Redis Token Bucket
Redis Sliding Window
Sentinel
Bucket4j
Guava RateLimiter
5. Agent 状态机
Agent 在系统里不应该是一个黑盒,而应该有明确状态:
text
CREATED
↓
IDLE
↓
THINKING
↓
TOOL_CALL
↓
STREAMING
↓
FINISHED / FAILED
对应含义:
| 状态 | 含义 |
|---|---|
| CREATED | 会话已创建 |
| IDLE | 空闲,等待用户输入 |
| THINKING | LLM 正在推理 |
| TOOL_CALL | 正在执行工具 |
| STREAMING | 正在向前端流式输出 |
| FINISHED | 正常完成 |
| FAILED | 执行失败 |
状态机的价值在于:
text
知道 Agent 卡在哪一步
支持取消
支持重试
支持超时控制
支持故障恢复
支持审计追踪
6. Session 和 User ID 如何绑定?
需要区分三个概念:
text
User ID:用户
Session ID:一次会话
Agent ID:具体 Agent 类型
关系是:
text
一个 User 可以有多个 Session
一个 Session 可以绑定一个 Agent
一个 Agent 可以被多个 User 使用
Redis 结构可以这样设计:
text
HSET session:meta:{session_id}
user_id -> usr_10024
agent_id -> agt_chat_pro
tenant_id -> tenant_a
created_at -> timestamp
ZADD user:sessions:{user_id} {timestamp} {session_id}
这样可以快速查:
text
这个 session 属于哪个 user
这个 user 有哪些历史 session
这个 session 用的是哪个 agent
7. Agent 工具调用完整流程
工具调用流程一般是:
text
用户输入
↓
LLM 决策是否调用工具
↓
后端拦截 tool_calls
↓
执行工具
↓
工具结果回注给 LLM
↓
LLM 生成最终回复
↓
SSE 流式返回前端
LLM 可能输出:
json
{
"tool_calls": [
{
"tool_name": "web_search",
"arguments": {
"query": "Agent architecture"
}
}
]
}
后端不要直接相信,要做:
text
工具名校验
参数 Schema 校验
权限校验
租户校验
超时控制
幂等控制
审计日志
工具结果回注:
json
{
"role": "tool",
"tool_name": "web_search",
"content": "搜索结果..."
}
8. SSE 如何交互?
SSE 全称:
text
Server-Sent Events
它是服务端向前端单向持续推送数据的协议。
前端请求:
http
GET /api/agent/chat/stream
Accept: text/event-stream
后端响应头:
http
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no
其中 X-Accel-Buffering: no 很关键,用来避免 Nginx 缓冲导致前端收不到实时 token。
SSE 格式:
text
event: message_chunk
data: {"delta":"你好"}
event: message_chunk
data: {"delta":",我是 Agent"}
event: done
data: [DONE]
注意每个事件之间必须有两个换行符:
text
\n\n
9. Agent 工具调用时 SSE 事件字段
基础字段:
text
event
tool_name
arguments
output
工程上建议扩展:
json
{
"event": "tool_start",
"session_id": "sess_123",
"message_id": "msg_456",
"trace_id": "trace_789",
"tool_call_id": "call_001",
"tool_name": "web_search",
"arguments": {
"query": "Agent architecture"
},
"status": "running",
"timestamp": 1720000000
}
工具结束:
json
{
"event": "tool_output",
"tool_call_id": "call_001",
"tool_name": "web_search",
"output": "工具返回结果...",
"status": "success",
"latency_ms": 820
}
工具异常:
json
{
"event": "tool_error",
"tool_call_id": "call_001",
"tool_name": "web_search",
"error_code": "TIMEOUT",
"error_message": "tool execution timeout",
"status": "failed"
}
10. 小结
这一部分可以归纳为:
Agent 平台可以拆成几层:最外层是统一协议层,屏蔽不同模型厂商接口差异;中间是模型适配器层,对 OpenAI、Claude、Qwen、DeepSeek 等模型做 Adapter 封装;再往下是智能路由层,根据租户配置、模型能力、成本、延迟和限流情况动态选择模型;最后是熔断和降级链路,保证主模型异常时可以切到备用模型。
多租户方面,每个请求都必须携带 tenant_id、user_id、session_id、agent_id 和 trace_id,通过上下文传播机制贯穿模型调用、工具调用、记忆检索和 SSE 推流链路。同时在网关层做租户级、用户级和模型级限流,避免某个租户突发流量拖垮全局服务。
Agent 本身要设计成状态机,例如 CREATED、IDLE、THINKING、TOOL_CALL、STREAMING、FINISHED 和 FAILED,这样后端可以监控 Agent 当前执行阶段,支持取消、重试、超时和故障恢复。
工具调用流程上,LLM 只负责生成 tool_calls,后端要做工具名校验、参数 Schema 校验、权限校验、超时控制、幂等和审计,然后将工具结果作为 tool 消息回注给模型生成最终答案。前端交互通常采用 SSE,服务端通过 event 和 data 分块推送模型 token、工具开始、工具输出、错误和完成事件。
五、GraphRAG 什么时候必须用?
1. 普通 Vector RAG 的本质问题
普通 RAG 的主链路是:
text
文档切 chunk
↓
生成 embedding
↓
向量相似度召回
↓
Rerank
↓
塞给 LLM 生成答案
它适合回答:
text
公司几号放假?
某个产品价格是多少?
某份文档里有没有某条规定?
因为这些问题通常能在某几个 chunk 里直接找到答案。
但是它的问题是:
它找的是"相似文本",不是"结构关系"。
普通 RAG 容易出现:
text
孤立匹配
无法连点成线
信息丢失
可以这样类比:
普通 RAG 更像从语料中找相似片段,GraphRAG 则是在关系网络中寻找路径。
2. GraphRAG 相比 Vector RAG 增加了哪些能力?
GraphRAG 不只是向量检索,而是多了一层知识图谱结构。
入库时除了切 chunk、做 embedding,还会抽取:
text
实体 Entity
关系 Relationship
属性 Attribute
社区 Community
摘要 Summary
比如文档里有:
text
A 公司投资了 B 公司
B 公司控股 C 公司
C 公司参与了某个项目
GraphRAG 在实体抽取和关系抽取之后,可以构建如下关系图:
text
A 公司 -> 投资 -> B 公司 -> 控股 -> C 公司 -> 参与 -> 项目 X
这样查询时就可以沿着图谱做多跳推理。
3. 四类适合 GraphRAG 的场景
场景一:跨文档全局总结
比如:
text
过去三年公司主要风险趋势是什么?
这不是某一个 chunk 能回答的,需要跨多个文档、章节、时间段做归纳。
GraphRAG 可以通过:
text
社区检测
Community Summary
Global Search
生成宏观视角。
场景二:长链条多跳推理
比如:
text
A 公司的投资人关联了哪些下游项目?
它需要走关系链:
text
A -> 投资 -> B -> 控股 -> C -> 参与 -> 项目 D
普通 RAG 可能会断链,GraphRAG 可以沿图谱关系逐层扩展。
场景三:高密度实体消歧
比如文档里有:
text
张总
张某某
张三
Zhang San
董事长张先生
普通 RAG 容易混淆。
GraphRAG 可以做:
text
Entity Resolution
实体融合
实体消歧
别名归一
典型场景:
text
金融风控
法律案件
企业尽调
供应链分析
医疗知识库
科研文献分析
场景四:隐性关系挖掘
比如:
text
两家公司没有直接合作记录,但是否存在潜在关联?
GraphRAG 可能发现:
text
A 公司 -> 投资 -> C 基金
B 公司 -> 被 C 基金投资
于是推断出:
text
A 和 B 通过 C 基金存在间接关联
4. 什么时候不用 GraphRAG?
GraphRAG 成本更高,需要:
text
实体抽取
关系抽取
图谱构建
实体消歧
社区检测
图检索
图摘要
简单事实没必要用。
Vector RAG 更适合
text
公司几号放假?
报销标准是多少?
某个 API 参数怎么写?
某份 PDF 里提到了什么条款?
Text-to-SQL 更适合
text
研发部门共有多少名员工?
上个月订单总额是多少?
A 类客户平均客单价是多少?
5. 工程最优解:混合路由
不是所有问题都走 GraphRAG,而是用 Query Router 做智能路由。
text
用户问题
↓
Query Router
↓
选择检索链路
| 问题类型 | 推荐链路 |
|---|---|
| 简单事实问答 | Vector RAG |
| 复杂关系 / 全局问题 | GraphRAG |
| 精确统计 | Text-to-SQL |
| 最新信息 | Web Search |
| 工具执行 | Agent Tool Calling |
整体架构:
text
文档解析
↓
Chunk 切分
├─ 生成 Embedding -> VectorDB
└─ 抽取实体关系 -> GraphDB
↓
实体消歧 / 社区检测 / 社区摘要
↓
Query Router
├─ Vector Search
├─ Graph Local Search
├─ Graph Global Search
└─ Text-to-SQL
↓
Rerank / 证据组装
↓
LLM 生成答案
6. Local Search 和 Global Search
Local Search
适合围绕具体实体展开:
text
A 公司有哪些合作方?
张三参与过哪些项目?
某个产品依赖哪些供应商?
它从一个实体节点出发,沿着一跳、二跳关系扩展。
Global Search
适合全局总结:
text
过去三年公司的主要风险趋势是什么?
这个行业的主要技术路线有哪些?
企业知识库中有哪些核心主题?
它依赖:
text
Community Detection
Community Summary
全局图谱摘要
7. 小结
这一部分可以归纳为:
GraphRAG 不是普通 RAG 的简单升级,而是为了解决普通向量检索无法表达结构关系的问题。普通 Vector RAG 主要依赖语义相似度召回 chunk,适合局部事实问答;但当问题涉及跨文档总结、多跳关系推理、实体消歧、隐性关系挖掘时,单纯向量召回很容易断链或者召回不完整。
GraphRAG 的核心是在入库阶段用大模型抽取实体、关系和属性,构建知识图谱,并进一步做实体融合、社区检测和社区摘要。查询时可以根据问题类型选择 Local Search 或 Global Search:Local Search 适合围绕具体实体做关系扩展,Global Search 适合跨文档、跨社区的全局总结。
但工程上不会让所有问题都走 GraphRAG,因为它构建成本、存储成本和检索复杂度都更高。更合理的方案是做 Query Router:简单事实类问题走 Vector RAG,复杂关系和全局问题走 GraphRAG,精确统计类问题走 Text-to-SQL。这样才能兼顾成本、延迟和回答质量。
核心结论:
text
普通 RAG 找相似片段,GraphRAG 找实体关系。
普通 RAG 捞出来的是点,GraphRAG 连接起来的是网。
简单事实用 Vector RAG,复杂关系用 GraphRAG,精确统计用 Text-to-SQL。
六、多模态 RAG 图片检索如何实现?
1. 为什么"图转文"不够?
传统方案是:
text
图片 / PDF 截图
↓
OCR 识别文字
↓
文本向量
↓
文本检索
这个方案对纯文字截图还行,但对复杂图片、表格、图表、流程图、公式、手写批注就容易失效。
主要问题有三个。
信息损失严重
OCR 会丢失:
text
表格行列结构
流程图箭头关系
公式空间布局
图表坐标轴关系
颜色、圈注、标记
手写批注位置
比如:
text
A -> B -> C
OCR 后可能变成:
text
A B C
箭头方向和层级关系都没了。
描述瓶颈
如果让 VLM 先生成 caption:
text
图片 -> VLM 描述 -> 文本向量检索
caption 本身是压缩信息。
如果用户问:
text
第三季度蓝色柱子为什么下降?
但 caption 里没提"第三季度""蓝色柱子""下降",就检索不到。
细粒度检索失败
比如:
text
找出 PDF 里带红色手写批注的那张流程图
哪个页面右下角有一个警告图标?
这种问题需要看局部视觉区域,不能只靠 OCR。
2. ColPali 类架构
ColPali 将整页文档作为视觉输入进行编码,而不是首先依赖 OCR 将图片转换为文本。
流程是:
text
PDF 页面 / 图片
↓
切成 Visual Patch
↓
每个 Visual Patch 编码成一个向量
↓
形成多向量表示
一页文档不是一个向量,而是一组向量:
text
page_3 = [v1, v2, v3, ..., vn]
好处是:
能保留图片内部的局部结构,而不是把整张图压缩成一个粗糙向量。
3. MaxSim 是什么?
MaxSim 可以理解为:
用户问题里的每个关键词,都去图片的所有视觉块里找最像的那个块,然后把这些最大相似度加起来。
比如用户问:
text
销售额增长趋势第四季度
Query token:
text
销售额
增长趋势
第四季度
图片 Visual Patch:
text
标题区域
折线图区域
表格区域
第四季度标注区域
说明文字区域
MaxSim 做的是:
text
"销售额" -> 找最匹配的 Visual Patch
"增长趋势" -> 找最匹配的 Visual Patch
"第四季度" -> 找最匹配的 Visual Patch
最后求和:
text
Score = max_sim(销售额, 所有 Visual Patch)
+ max_sim(增长趋势, 所有 Visual Patch)
+ max_sim(第四季度, 所有 Visual Patch)
这比"一张图片一个向量"更细粒度。
4. 多模态 RAG 图片检索流程
离线入库
text
PDF / 图片 / 截图
↓
页面渲染 / 图片标准化
↓
Visual Patch 切分
↓
VLM / ColPali 编码
↓
多向量索引
↓
元数据存储 + 原图存储
元数据包括:
json
{
"doc_id": "doc_001",
"page": 3,
"patch_id": "p_003_018",
"bbox": [120, 300, 420, 520],
"type": "chart",
"permission": "tenant_a"
}
在线查询
text
用户 Query
↓
Query 编码
↓
Metadata Filter
↓
多向量 MaxSim 检索
↓
Top-K 页面 / Patch 召回
↓
VLM 视觉重排序
↓
证据组装
↓
LLM / VLM 生成答案
5. 为什么还需要视觉重排序?
向量检索是粗筛,可能召回很多噪声。
所以需要:
text
视觉向量粗召回
↓
VLM / 多模态 Reranker 精排
这类似文本 RAG 里的:
text
Embedding 粗召回
↓
Reranker 精排
6. 工程优化
多模态检索成本很高,因为图片 Visual Patch 很多。
比如:
text
100 页 PDF
每页 500 个 Visual Patch
总共 50,000 个向量
所以需要优化。
向量量化压缩
text
FP32 -> INT8 / Binary / 1-bit / PQ
压缩向量用于粗召回,原始向量或 VLM 用于重排。
Metadata 过滤
先过滤:
text
文档类型
业务线
时间范围
权限
图片类型
再向量检索。
混合召回
同时使用:
text
OCR 文本召回
图片视觉向量召回
文档标题召回
元数据召回
业务标签过滤
7. 小结
这一部分可以归纳为:
多模态 RAG 图片检索不能简单依赖 OCR 或图转文,因为 OCR 会丢失版面结构、图表关系、公式空间信息和手写批注位置;而图片 caption 又会把视觉信息压缩成一段描述,容易出现表达瓶颈,无法支持细粒度查询。
更合理的方案是采用类似 ColPali 的视觉多向量架构。离线阶段先把 PDF 或图片按页面渲染成图像,再切成 Visual Patch,使用视觉语言模型把每个 Patch 编码成多向量,同时保存 doc_id、page_id、bbox、权限、来源等元数据。在线检索时,把用户 Query 编码成文本 token 向量,与图片 Patch 向量映射到统一语义空间,通过 MaxSim 计算 Query token 与视觉 Patch 的最大相似度,从而实现细粒度视觉区域召回。
召回后还需要使用轻量 VLM 或多模态 Reranker 做二次重排,过滤掉视觉相似但语义不匹配的噪声。工程上为了控制成本,需要结合向量量化、metadata 过滤、OCR 辅助召回和混合召回机制,避免全量图片向量库盲搜。
核心结论:
text
多模态图片 RAG 不等于 OCR + 文本检索。
更好的方案是:图片切 Patch,多向量编码,Query 与视觉 Patch 做 MaxSim 匹配。
工程落地要加:metadata 过滤、向量量化、混合召回、VLM 重排序。
七、Agent 长期记忆应该存什么?
1. 长期记忆不是聊天记录
Agent 长期记忆不是把所有对话都存下来。
长期记忆通常存储以下几类信息:
text
可复用的偏好
可确认的事实
可迁移的经验
可执行的规则
常见判断标准是:
这条信息未来再次遇到类似任务时,是否能帮助 Agent 更快、更准、更少犯错?
如果答案是"是",就值得写入长期记忆。
2. 哪些信息值得存?
用户明确偏好
比如:
text
用户希望用中文回答
用户偏好 Kotlin 示例
用户喜欢结构化、工程化的技术总结
用户写技术文章时偏好工程落地视角
要区分:
text
不该存:今天喝了冰美式
该存:用户偏好冰美式咖啡
关键是稳定性和可复用性。
确认过的事实数据
比如:
text
用户项目使用 Kotlin + Android
某个系统部署在 Kubernetes 上
某个业务规则已经被用户确认
某个项目的线上环境变量叫 ENV_X
不该存:
text
明天要去北京出差
这次任务临时用这个参数
今天会议改到下午三点
这些属于短期上下文,不是长期记忆。
高复用踩坑经验
这是长期记忆里很有工程价值的一类。
比如:
text
处理这个项目时,必须先读取 common/config
biometricFlags 为空时不能沿用旧值
这个仓库测试命令需要先设置环境变量 X
用户不喜欢只讲概念,需要结合工程落地讲
它本质上是:
text
过去任务中验证过的经验
能减少 Agent 下次重复踩坑。
业务规则 / 工程规则
比如:
text
支付相关操作必须人工确认
生产环境变更必须走审批
用户数据必须脱敏
某类任务必须先检索知识库
某类任务不能直接调用高风险工具
这类应该存成规则型记忆,优先级更高。
长期项目背景
比如:
text
项目名称
技术栈
模块边界
关键仓库
核心业务流程
已知历史问题
架构约束
这样用户下次说:
text
继续看上次那个配置问题
Agent 才能接上上下文。
3. 哪些信息不该存?
text
一次性的闲聊
临时状态
未经确认的猜测
模型自己推断但用户没确认的信息
完整原始聊天记录
敏感隐私信息
过期事实
低价值工具输出
长期记忆最怕两件事:
text
写入太多,污染召回
写入错误,持续误导后续行为
4. 什么时候触发写入?
不是每轮对话都写。
合理触发时机:
| 触发时机 | 是否适合写入 |
|---|---|
| 用户明确说"记住" | 强写入 |
| 用户多次确认某个偏好 | 可写入 |
| 任务完成后沉淀经验 | 可写入 |
| 工具调用发现稳定配置 | 可写入 |
| LLM 自己猜测 | 不应直接写入 |
| 一次性临时信息 | 不应写入 |
流程:
text
用户对话
↓
记忆候选抽取
↓
价值判断
↓
冲突检测
↓
确认 / 自动写入
↓
长期存储
5. 三层记忆结构
热记忆
text
当前 Session 上下文
当前任务状态
本轮工具结果
最近几轮摘要
通常放在:
text
LLM Context Window
Redis
本地内存
温记忆
text
近期历史多轮对话摘要
最近几天或几周任务记录
通常通过向量库语义召回。
冷记忆
text
长期稳定偏好
项目事实
业务规则
高复用经验
通常放在:
text
关系型数据库
向量数据库
图数据库
规则库
6. 记忆数据结构
一条长期记忆不要只存一句话,应该带元数据:
json
{
"memory_id": "mem_001",
"tenant_id": "tenant_a",
"user_id": "user_123",
"type": "preference",
"content": "用户偏好用中文解释技术问题,并希望结合工程落地。",
"source": "explicit_user_feedback",
"confidence": 0.95,
"importance": 0.9,
"created_at": "2026-07-08",
"updated_at": "2026-07-08",
"expires_at": null,
"status": "active",
"embedding": "..."
}
关键字段:
| 字段 | 作用 |
|---|---|
| type | 偏好、事实、经验、规则 |
| source | 记忆来源 |
| confidence | 置信度 |
| importance | 重要性 |
| expires_at | 过期时间 |
| status | active / deprecated / conflict |
| embedding | 语义召回 |
7. 冲突和遗忘
长期记忆不是永久正确的。
比如旧记忆:
text
用户工作地点在上海
新信息:
text
用户现在长期在北京办公
不能简单追加。
应该:
text
新旧信息对比
↓
判断是否同一属性
↓
新时间戳覆盖旧事实
↓
旧记忆标记 deprecated
遗忘机制包括:
text
热度衰减
推翻淘汰
泛化压缩
用户主动删除
8. 小结
这一部分可以归纳为:
Agent 长期记忆不应该等同于保存完整聊天记录,而应该保存那些未来能持续影响 Agent 行为的信息。一般包括四类:第一类是用户明确偏好,比如输出语言、代码风格、沟通习惯;第二类是经过确认的稳定事实,比如项目技术栈、业务规则、工作环境;第三类是高复用踩坑经验,比如某个系统的特殊配置、失败原因和修复方式;第四类是长期项目背景和规则型知识,比如权限约束、审批流程和工具调用边界。
写入时不能无脑存,应该有触发条件,例如用户明确要求记住、任务闭环后沉淀经验、多轮对话中反复确认,或者模型通过自评认为该信息对未来任务有复用价值。写入前还要做价值判断、去重、冲突检测和敏感信息过滤。
工程上可以把记忆分成三层:热记忆放当前 Session 上下文,服务当前任务;温记忆存近期对话摘要,通过向量库召回;冷记忆存长期稳定的用户偏好、项目事实和业务规则,放在关系型数据库、向量库或图数据库中。每条记忆都要带 type、source、confidence、importance、timestamp、expires_at、status 等元数据,便于召回、衰减和冲突处理。
核心结论:
text
稳定偏好、确认事实、高复用经验值得存;
临时状态、原始聊天、未经确认的猜测不该存。
八、Agent 把错误信息写进记忆后怎么办?
1. 这属于典型的 Memory Pollution(记忆污染)问题
普通缓存异常可能是:
text
Redis 某个 Session 状态错了
临时上下文脏了
某次工具调用结果异常
处理方式一般是:
text
清缓存
重启会话
重新执行
但错误记忆不一样。
如果 Agent 错误记住:
text
用户偏好英文回答
某项目必须使用环境变量 X
这个接口返回 null 时应该沿用旧值
用户工作地点是北京
它会在后续任务里被反复召回,持续影响推理。
所以它不是简单的数据错了,而是:
Agent 的认知层被污染了。
2. 处理步骤
第一步:隔离错误记忆
不要先物理删除,而是标记状态:
text
active -> quarantined / deprecated / invalid
比如:
json
{
"memory_id": "mem_001",
"status": "quarantined",
"reason": "user_reported_incorrect",
"confidence": 0.05
}
这样后续召回不会再用它,同时保留审计记录。
第二步:全链路溯源
要查清楚:
text
是谁写入的?
什么时候写入的?
根据哪段对话写入的?
基于哪个工具结果写入的?
被哪些后续任务使用过?
影响了哪些决策?
所以记忆必须带:
text
memory_id
trace_id
source
derived_from
used_by
created_at
updated_at
第三步:判断是否已经被使用
如果只是写入但没被使用:
text
隔离 / 删除 / 重写即可
如果已经被使用,就要做影响面分析:
text
链路定位
↓
影响面分析
↓
可逆操作回滚
↓
不可逆操作补偿
↓
认知重校
比如:
| 情况 | 处理方式 |
|---|---|
| 错误影响了回答 | 更正答案 |
| 错误影响了代码修改 | 回滚 commit 或提交修复 PR |
| 错误影响了配置 | 恢复配置快照 |
| 错误影响了外部发送行为 | 发补偿说明或人工介入 |
| 错误影响了多个任务 | 批量扫描相关 trace |
3. 如何防止错误信息写入?
长期记忆写入必须有准入控制。
可以设计四道门:
text
候选抽取
↓
价值判断
↓
冲突检测
↓
高风险人工确认
候选抽取
Agent 先生成候选,不直接写入。
价值判断
判断:
text
是否稳定?
是否可复用?
是否被确认?
是否会影响未来决策?
是否可能过期?
冲突检测
比如已有:
text
用户工作地点:上海
新候选:
text
用户工作地点:北京
不能直接追加,要判断是更新、临时出差,还是冲突。
人工确认
高风险记忆不能自动写:
text
生产环境规则
支付规则
权限规则
用户身份信息
工具调用约束
长期业务策略
4. 记忆系统本质是配置层
Agent 长期记忆不是普通聊天记录,而是一种动态配置层。
它会影响:
text
怎么回答
调哪个工具
查哪个知识库
是否跳过某步骤
是否采用某个历史经验
所以要像治理配置一样治理记忆:
text
有版本
有来源
有状态
有权限
有回滚
有审计
有灰度
有失效机制
5. 小结
这一部分可以归纳为:
如果 Agent 把错误信息写入长期记忆,不能简单地清缓存或删除一条记录,因为长期记忆会跨会话影响后续推理和工具调用,这属于记忆污染。
处理时可以先把这条记忆从 active 状态切到 quarantined 或 deprecated,阻止后续召回;然后通过 memory_id、trace_id、source、derived_from 等元数据做全链路溯源,找出它是由哪段对话、哪个工具结果或哪个模型推断写入的,并检查它是否已经被后续任务使用。
如果只是写入但未使用,可以直接失效并重写;如果已经被使用,就要做影响面分析,对可逆操作执行回滚,对不可逆操作创建补偿任务,比如重新生成结果、通知用户或进入人工审核。
从架构上,长期记忆必须有防污染机制:写入前做候选抽取、价值判断、冲突检测、敏感信息过滤和高风险人工确认;存储上要做索引与内容分离、版本管理、状态标记、快照回滚和访问审计。
所以记忆系统应该被当成动态配置层治理,而不是简单的历史聊天记录库。核心原则是:防重于治,溯源回滚是核心,用户必须拥有最终解释权和删除权。
核心结论:
text
先止血:禁用错误记忆
再溯源:查它怎么来的
再修复:回滚或补偿受影响任务
再防御:加写入校验和人工确认
九、Agent 幂等性怎么设计?
1. 先讲清楚定义
Agent 场景下的幂等性通常定义为:
同一个任务因为重试、恢复、超时、消息重复、回调重复等原因被执行多次时,对外只产生一次有效副作用。
一句话:
text
Agent 幂等性 = 重复执行,不重复生效。
2. 为什么 Agent 更容易出问题?
Agent 不是普通接口调用,而是一个多步骤执行链路:
text
规划任务
↓
调用工具
↓
等待外部结果
↓
超时重试
↓
断点恢复
↓
继续执行后续步骤
每一步都可能重复。
比如 Agent 帮用户订票:
text
创建订单
扣库存
支付
发送短信
写数据库
如果网络超时,Agent 以为失败,然后重试一次。
没有幂等就会出现:
text
重复下单
重复扣库存
重复扣款
重复发短信
重复写入业务数据
3. 三层设计
工程设计不能只停留在一句"加幂等 key",而应该拆成三层设计:
text
请求层:唯一标识
工具层:副作用去重
流程层:状态机 + Checkpoint
4. 第一层:请求层唯一 ID
每个任务都要有唯一标识:
text
request_id
idempotency_key
task_id
tool_call_id
trace_id
比如:
json
{
"request_id": "req_123",
"session_id": "sess_456",
"user_id": "user_001",
"tool_call_id": "call_789",
"action": "create_order"
}
服务端收到请求后,先查:
text
这个 request_id 有没有处理过?
如果处理过,直接返回之前结果,不重新执行。
5. 第二层:工具层副作用去重
Agent 最危险的是工具调用,尤其是:
text
下单
支付
发短信
发邮件
写数据库
修改配置
提交代码
删除数据
这些都是有副作用操作。
工具层要做:
text
先查状态,再决定是否执行。
去重表可以这样设计:
sql
CREATE TABLE idempotency_record (
id BIGINT PRIMARY KEY,
request_id VARCHAR(128) UNIQUE,
action_type VARCHAR(64),
status VARCHAR(32),
result_json TEXT,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
关键是:
text
UNIQUE(request_id)
数据库层要兜底,不能只靠应用层判断。
6. 第三层:流程层状态机 + Checkpoint
Agent 是多步骤任务,不能只在接口层防重。
比如:
text
Step 1:创建订单成功
Step 2:扣库存成功
Step 3:支付超时
Step 4:Agent 重试
重试时不能从头再跑。
应该有状态机:
text
INIT
↓
ORDER_CREATED
↓
STOCK_LOCKED
↓
PAYMENT_PENDING
↓
PAYMENT_SUCCESS
↓
FINISHED
每一步完成后写 Checkpoint:
json
{
"task_id": "task_001",
"current_step": "STOCK_LOCKED",
"order_id": "order_123",
"stock_locked": true,
"payment_status": "pending"
}
恢复时从 Checkpoint 继续:
text
已经创建订单 -> 不再创建
已经扣库存 -> 不再扣
支付未完成 -> 查询支付状态或继续处理
7. 真正难点
LLM 输出不稳定
同一个任务重试时,模型可能生成不同参数。
第一次:
json
{"city": "北京", "date": "明天"}
第二次:
json
{"city": "北京市", "date": "2026-07-09"}
所以第一次规划出的步骤和参数要保存成 Plan / Checkpoint。
恢复时使用旧 Plan,而不是让 LLM 重新自由生成。
成功但返回失败
比如外部支付系统已经扣款成功,但响应超时。
正确做法是:
text
重试前先查询外部系统状态
而不是直接重新支付。
长链路部分成功
text
创建订单成功
扣库存成功
发短信失败
恢复时只重试短信,不重跑整个流程。
并发重复触发
多个 Worker 同时消费同一条消息。
需要:
text
分布式锁
数据库唯一索引
消息去重
状态机 CAS 更新
例如:
sql
UPDATE task
SET status = 'RUNNING'
WHERE task_id = 'task_001'
AND status = 'PENDING';
只有一个 Worker 能更新成功。
8. 不是追求 exactly-once
分布式系统里很难真正做到 exactly-once。
工程上更常见的是:
text
at-least-once 执行 + 幂等去重
也就是:
text
允许消息至少执行一次
允许系统重试
但每次执行前都检查是否已经处理过
保证最终副作用只发生一次
9. 小结
这一部分可以归纳为:
Agent 幂等性指的是同一个任务因为重试、超时、恢复、消息重复或者回调重复被执行多次时,对外只产生一次有效副作用。它不是不允许重试,而是允许重复请求,但保证结果和执行一次一致。
Agent 比普通接口更容易出问题,因为它是多步骤链路,包含规划、工具调用、外部系统回调、断点恢复和消息队列消费。任何一步都可能重复触发,尤其是下单、支付、发消息、写库这类有副作用操作,如果没有幂等设计,就会产生重复下单、重复扣款、重复写入。
设计上可以分三层:第一是请求层,每个任务或关键操作生成 request_id 或 idempotency_key;第二是工具层,对有副作用的 Tool Call 做去重表和数据库唯一约束,执行前先查状态,已成功则直接返回历史结果;第三是流程层,用状态机和 Checkpoint 记录每一步执行状态,失败恢复时只继续未完成步骤,不重复执行已成功动作。
对于外部系统成功但响应失败的情况,不能盲目重试,而要先查询外部状态;对于不可逆操作,需要设计补偿机制,比如取消、回滚或人工处理。工程上更常见的是 at-least-once 执行 + 幂等去重,而不是追求理想化的 exactly-once。核心目标是让 Agent 在失败和重试之后,结果仍然一致、可控、可追踪。
核心结论:
text
请求层唯一 ID,工具层副作用去重,流程层状态机 + Checkpoint。
目标不是避免所有失败,而是让失败恢复后结果一致、可控、可追踪。
十、总结
这些 Agent 工程主题表面上属于不同方向:
text
Agentic Workflow
模型量化
多头注意力
多模型平台
GraphRAG
多模态 RAG
长期记忆
记忆污染
幂等性
但底层共同指向一个能力:
是否能把大模型应用从 Demo 级调用,提升到工程级系统设计。
这一部分可以归纳为:
text
Workflow 提供流程编排与状态控制;
Agent 提供推理、规划和工具调用能力;
RAG 提供外部知识检索;
GraphRAG 提供结构化关系推理;
多模态 RAG 提供视觉信息检索;
长期记忆提供跨会话经验沉淀;
记忆治理负责冲突检测、版本管理与污染控制;
幂等性保证失败恢复后的副作用一致性;
多租户平台提供资源隔离、配置管理和运行治理。
最后可以用一句话收尾:
AI Agent 的工程化建设并不是单纯依赖模型能力,而是在流程编排、工具治理、知识检索、长期记忆、状态管理和可靠性机制等基础设施之上,充分发挥大模型在复杂推理和决策场景中的能力。