如何把大模型从"会回答问题",设计成一个可执行、可扩展、可校验、可观测、可优化的工程系统。
很多概念不能只停留在一句话解释上。比如"RAG 就是检索增强生成""Function Calling 就是调用函数""Skill 就是工具",这些说法都不算错,但对于真实工程系统来说还不够。更合理的分析方式是:
text
先了解清楚问题本质;
再说明它在系统链路中的位置;
然后展开工程实现;
再补充风险、兜底和评估指标;
最后用传统工程做类比。
多 Agent 协同场景下,如何保证跨 Agent 结构化数据传递的稳定性?
多 Agent 协同里,最容易出问题的地方不是某一个 Agent 回答错了,而是上游 Agent 把一段不稳定、不完整、不可验证的内容传给了下游 Agent,导致下游继续基于错误信息执行,最后形成一连串级联错误。
所以在多 Agent 系统里,Agent 之间最好不要直接传递大段自然语言。自然语言对人来说容易理解,但对程序来说不稳定,很容易出现字段缺失、格式漂移、JSON 不合法、Markdown 污染、语义不一致等问题。
更稳的做法,是把 Agent 之间的通信设计成一条结构化数据链路。每个 Agent 的输出都应该符合明确的 Schema,并且在进入下游之前经过校验。
整体可以分成三层:
text
模型层:尽量让模型输出符合结构化格式;
工程层:对模型输出做强校验和修复;
架构层:用状态机、黑板或消息总线隔离 Agent 状态。
模型层主要解决"让模型尽量少出错"的问题。可以使用 Function Calling、Tool Calling、Structured Output 或 Constrained Decoding,让模型不要自由生成一大段文本,而是按照预先定义好的参数结构输出。
工程层解决"模型输出不能直接相信"的问题。即使模型看起来输出了 JSON,也要做反序列化和 Schema 校验。比如检查字段是否完整、类型是否正确、枚举值是否合法、嵌套结构是否符合要求、业务逻辑是否成立。
例如一个 Agent 输出:
json
{
"task_id": "T001",
"status": "success",
"confidence": 0.91,
"result": {
"summary": "xxx",
"next_action": "call_search_tool"
}
}
系统不能只检查它是不是合法 JSON,还要继续检查:
text
task_id 是否存在;
status 是否只能是 success / failed / need_retry;
confidence 是否在 0 到 1 之间;
next_action 是否是允许的动作;
result 是否符合当前 Agent 的输出协议。
如果校验失败,不应该直接让系统崩掉,也不应该无限重试。更合理的做法是设置一个有限的修复流程:
text
第一次失败:把校验错误反馈给模型,让模型修复结构;
第二次失败:降低任务复杂度,只重新生成局部字段;
第三次失败:进入兜底策略,比如跳过节点、人工审核、降级执行。
校验错误可以这样反馈给模型:
text
你的输出无法通过 Schema 校验。
错误原因:
1. 字段 user_id 缺失
2. status 只能是 pending / success / failed
3. confidence 必须是 number 类型
请只返回修复后的 JSON,不要添加 Markdown。
这类修复可以理解为 Reflection / Self-Correction,但它不能无限循环。一般要设置最大重试次数,比如 2 到 3 次,超过之后就应该降级、跳过或转人工。
架构层则要避免 Agent 之间点对点传递自由文本。更稳的方式是让它们通过共享状态区、黑板模式、消息总线或工作流状态表协作。
一个比较合理的结构是:
text
Agent A
↓
结构化消息
↓
Schema 校验
↓
状态存储 / 黑板 / 消息总线
↓
Agent B
↓
结构化消息
↓
Schema 校验
↓
Agent C
中间状态层可以包含这些字段:
text
task_id:任务 ID;
agent_id:当前 Agent;
schema_version:协议版本;
input_ref:输入来源;
output:结构化输出;
confidence:置信度;
status:执行状态;
error_code:错误码;
trace_id:链路追踪 ID;
timestamp:时间戳。
这样每个 Agent 只处理自己负责的字段,出错后也能定位是哪一步出了问题。协议升级时,也可以通过 schema_version 做兼容。
用传统工程类比,这就像服务之间通过 DTO、接口协议和参数校验通信。Java 里不会让一个模块返回一段自然语言给另一个模块解析,而是会定义明确的 DTO、字段类型、枚举值、异常码和校验逻辑。多 Agent 系统也是一样,只是输出来源从普通代码变成了大模型,所以更需要强校验和容错。
对应关系大概是:
text
Agent 输出 ≈ 外部接口返回值;
Schema ≈ DTO / VO / Protobuf 定义;
JSON Schema 校验 ≈ 参数校验 / Bean Validation;
Reflection 修复 ≈ 异常捕获后的重试和修正;
状态机 / 黑板 ≈ 工作流状态表 / 中间状态存储;
Trace ID ≈ 分布式链路追踪;
兜底逻辑 ≈ 降级 / 熔断 / Dead Letter Queue。
所以,多 Agent 结构化数据传递的核心不是"让模型看起来输出 JSON",而是要把 Agent 之间的通信做成一条可验证、可恢复、可追踪的工程链路。模型负责生成结构化候选结果,工程系统负责校验、修复、状态管理和异常兜底。
ReAct Agent 解决复杂问题的核心抓手是什么?
ReAct 的核心抓手,是把大模型从一次性生成答案,变成一个"推理 Thought → 行动 Action → 观察 Observation"的动态闭环。
ReAct Agent 解决复杂问题的关键,是让模型在推理过程中不断决定下一步行动,通过工具与外部环境交互,再根据观察结果修正自己的推理路径。
可以简单表示为:
text
Thought:先分析当前问题,判断下一步该做什么;
Action:调用搜索、数据库、API、代码解释器等工具;
Observation:读取工具返回结果,再更新自己的判断。
普通 LLM 的工作方式通常是:
text
用户提出问题
↓
模型基于已有上下文一次性生成答案
这种方式适合简单问答,但面对复杂任务时会有明显局限,比如:
text
信息不完整;
需要实时数据;
需要调用多个工具;
中间结果会影响下一步;
任务步骤无法提前完全确定。
ReAct 的流程则更像这样:
text
用户任务
↓
Thought:我现在需要先判断问题类型
↓
Action:调用搜索工具 / 数据库 / API / 代码解释器
↓
Observation:得到外部结果
↓
Thought:根据结果调整下一步
↓
Action:继续执行
↓
Observation:继续反馈
↓
Final Answer:输出最终结果
它不是一次性"猜"出答案,而是通过外部反馈不断收敛。
Thought 负责让 Agent 判断当前任务是什么、已经知道什么、缺什么信息、下一步调用什么工具、工具参数怎么填、是否可以输出最终答案。这里不要把 Thought 简单理解成"模型的内心独白"。工程上更重要的是:Thought 代表 Agent 对当前状态的分析和下一步动作选择。
Action 是 ReAct 和普通问答最大的区别之一。普通 LLM 只能生成文本,而 ReAct Agent 可以调用外部工具,比如:
text
搜索引擎;
数据库;
私有 API;
代码解释器;
文件系统;
浏览器;
业务系统;
RAG 检索器;
计算工具。
Action 的意义是把模型的"想法"转化成对外部世界的操作。也就是说,LLM 不再只是"会说",而是开始"能做"。
Observation 是工具执行后的返回结果。模型拿到 Observation 后,会重新判断:
text
这个结果是否满足需求;
是否需要继续查询;
是否发现了新的问题;
是否需要换工具;
是否需要修正前面的判断;
是否可以结束任务。
Observation 的价值,是给模型提供外部反馈,让它能够纠错和动态调整,而不是一路按最初的猜测走到底。
ReAct 可以概括为:
text
ReAct = Reasoning + Acting
更工程化地说,它是一种把 LLM 决策能力、工具调用能力和环境反馈机制组合起来的 Agent 执行框架。它不是单纯的 Prompt 技巧,而是一种 Agent 工作流范式。
Function Calling 更像是:
text
模型判断需要调用哪个函数
↓
填好参数
↓
函数执行
↓
模型基于结果回答
而 ReAct 更强调多轮循环:
text
Thought
↓
Action
↓
Observation
↓
Thought
↓
Action
↓
Observation
↓
Final Answer
所以 Function Calling 是工具调用能力,ReAct 是围绕工具调用构建的一种推理-行动闭环。Function Calling 可以作为 ReAct 中 Action 的实现方式。
ReAct 虽然强,但工程上也有几个坑。
第一是无限循环。Agent 可能一直重复搜索、重复调用工具、反复修正但不结束。解决方式是设置 max_iterations,记录历史 action,检测重复动作和重复参数,超过阈值后强制总结或降级。
第二是上下文爆炸。ReAct 每一轮都会产生 Thought、Action、Observation,轮数多了上下文会迅速膨胀。解决方式是压缩 Observation,只保留核心结果,用滑动窗口保留最近 K 轮,把长期信息写入 memory 或 state。
第三是工具调用失败。工具可能出现参数错误、API 超时、权限不足、返回空结果等问题。解决方式是工具参数 Schema 校验、错误码标准化、失败后允许模型修正参数,并设置重试次数。
第四是长链路推理错误级联。复杂任务里,前一步判断错了,后面可能全错。解决方式是引入 Reflection、关键节点校验、任务拆解、Planner,以及对高风险动作增加人工确认。
ReAct 可以类比成一个带状态的任务执行器:
text
Thought ≈ 决策逻辑 / Controller 判断下一步;
Action ≈ 调用 Service / RPC / DAO / 系统 API;
Observation ≈ 接口返回值 / 回调结果 / 异常信息;
Memory / State ≈ 上下文状态 / ViewModel / 状态机;
Max Iterations ≈ 循环退出条件 / 超时保护;
Reflection ≈ 异常处理后的重试与修正。
所以,ReAct 的价值不是单纯"会调用工具",而是把 LLM 的推理能力和工具的确定性执行能力结合起来,让复杂任务可以分步骤完成,并根据中间结果动态调整路径。

如何设计一套 Agent 的上下文取舍方案?
Agent 的上下文取舍,本质是成本、性能、准确性之间的平衡。上下文不是越多越好,而是要让模型在有限 Token 内看到当前任务最需要的信息。
我会把上下文分层管理,然后按四个维度筛选:
text
时效性 Recency;
相关性 Relevance;
重要性 Importance;
多样性 Diversity。
再结合滑动窗口、分层摘要、RAG 外部记忆和工具结果剪枝来控制上下文规模。
Agent 和普通聊天不一样。普通聊天可能只是几轮问答,而 Agent 执行任务时会不断产生中间信息:
text
用户原始需求;
任务拆解结果;
Planner 的计划;
每一轮 Thought;
每一次 Tool Call;
工具返回 Observation;
中间错误日志;
重试记录;
历史决策;
最终结果。
如果全部塞回上下文,会出现三个问题。
第一,性能下降。上下文越长,模型处理成本越高,响应速度也会变慢。
第二,成本失控。Agent 往往不是调用一次模型,而是多轮循环调用。如果每一轮都带完整历史,Token 成本会被放大。
第三,信息噪声。无关信息太多时,模型可能注意力分散、指令偏移、引用过期信息,甚至把中间日志当成最终事实。
时效性指最近发生的信息通常更重要,比如用户刚刚修改了需求,或者工具刚刚返回最新结果。但也不能只看最近,因为系统约束、用户最终目标、不可违反的业务规则即使很早出现,也必须保留。
相关性指和当前子任务最相关的信息优先保留。例如当前 Agent 正在生成 SQL,就应该保留表结构、字段含义、查询目标、过滤条件,而不是保留前面无关的闲聊或失败日志。
重要性指有些信息虽然不新,但非常关键,比如系统级指令、用户目标、安全约束、业务规则、关键决策依据、最终确认过的中间结论。这些信息应该进入任务状态或长期状态,而不是只靠滑动窗口保留。
多样性指上下文不能只保留一种路径的信息。在复杂任务中,可能有多个假设、多个候选方案、多个工具结果。为了避免模型过早陷入单一路径,可以保留不同方向的关键摘要。
动态滑动窗口是最基础的策略。比如:
text
保留最近 3~5 轮用户-模型交互;
保留最近 K 次 Tool Call 的核心结果;
删除过旧的冗余中间过程。
滑动窗口简单稳定,但早期关键约束可能会被滑出窗口,所以不能单独使用,必须配合摘要和状态管理。
对于较长历史,可以做滚动摘要。不要只总结"聊了什么",而要总结"后续决策需要什么"。例如把历史压缩成:
text
任务目标:xxx;
用户约束:xxx;
已确认方案:xxx;
未解决问题:xxx;
当前下一步:xxx。
一些长期背景、历史文档、工具结果、项目资料,不应该长期塞在 Prompt 里,而应该放到外部存储中,比如向量数据库、关系型数据库、KV 存储、对象存储、任务状态表或知识库。当前任务需要时,再通过 RAG 检索回来。
也就是说:
text
Prompt 里只放当前推理需要的信息;
外部存储里保存可回溯的完整信息。
工具返回结果往往很长,比如网页内容、数据库返回、日志输出等,不能原样塞回上下文。更合理的是保留来源、结论、关键证据、时间、置信度、错误码和引用位置,把完整结果放到外部存储。
可以把 Agent 上下文设计成五层:
text
L1:System / Developer 指令
永远保留,优先级最高。
L2:Task State 当前任务状态
包括目标、约束、进度、下一步计划。
L3:Working Memory 工作记忆
保留最近几轮交互和最近工具结果。
L4:Long-term Memory 长期记忆
用户偏好、项目背景、历史经验,按需检索。
L5:External Knowledge 外部知识
文档、数据库、网页、代码库、业务系统,按需 RAG 回捞。
最终 Prompt 不是简单拼接全部历史,而是动态组装:
text
当前 Prompt =
系统指令
+ 当前任务目标
+ 关键约束
+ 最近交互
+ 必要工具结果
+ RAG 检索出的相关记忆
+ 输出格式要求
上下文取舍不能只凭感觉,需要指标评估:
text
任务成功率 SR;
推理首字延迟;
Token 节省率;
关键约束保留率;
工具调用正确率。
Token 节省率可以这样计算:
text
Token 节省率 = (裁剪前 Token - 裁剪后 Token) / 裁剪前 Token
用传统工程类比,Agent 的上下文管理不应该像把所有日志都塞进一个 StringBuilder,而应该像一个有状态的任务系统:
text
System Prompt ≈ 全局配置 / 系统规则;
Task State ≈ ViewModel / 状态机;
Working Memory ≈ 当前页面临时状态;
Long-term Memory ≈ 本地数据库 / SharedPreferences / DataStore;
RAG 检索 ≈ 按需查询数据库;
Tool Observation ≈ 接口返回值;
Token 剪枝 ≈ 日志脱敏、裁剪、采样;
上下文组装 ≈ 根据当前业务场景构造 DTO。
传统工程里,我们不会把所有接口日志、历史页面状态、数据库内容全部传给一个方法。Agent 也是一样,应该传的是当前函数真正需要的参数。 
如何设计一个企业级知识库问答 Agent?
企业级知识库问答 Agent,本质上是一个以 RAG 为核心,结合多源数据处理、混合检索、重排序、权限控制、引用溯源和反馈闭环的问答系统。
整体可以分成四层:
text
第一层:数据入库层;
第二层:混合知识存储层;
第三层:在线问答引擎层;
第四层:安全与优化层。
整体链路是:
text
多源企业数据
↓
ETL 清洗与解析
↓
智能切分 Chunking
↓
Embedding 向量化
↓
向量数据库 + 全文索引 + 知识图谱
↓
用户提问
↓
意图识别与路由
↓
混合检索 Hybrid Search
↓
权限过滤 ACL
↓
Rerank 精排
↓
Prompt 组装
↓
LLM 生成答案
↓
引用溯源 + 安全校验
↓
反馈闭环优化
企业知识库问答效果不好,很多时候不是大模型不行,而是数据没洗干净、表格解析错了、Chunk 切得不合理、权限过滤没做好、召回不准、缺少 Rerank、Prompt 拼接不合理,或者答案没有引用和校验。
企业知识来源通常很复杂,包括:
text
PDF;
Word;
Excel;
PPT;
Wiki;
网页;
数据库;
业务系统 API;
客服工单;
会议纪要;
制度文档;
产品文档;
代码文档;
图片和扫描件。
入库时不能简单把文件丢给向量库,而是要先做 ETL,包括:
text
文本抽取;
OCR 识别;
版面分析;
表格提取;
图片说明提取;
页眉页脚清理;
目录和脚注处理;
重复内容去重;
乱码和无效字符清理;
文档结构还原;
元数据提取。
企业级知识库特别容易踩坑的是 PDF 解析。PDF 里经常有多栏排版、图片型扫描件、复杂表格、页眉页脚、脚注、跨页表格、图文混排、目录和正文混杂。如果这里处理不好,后面的 Embedding 和检索都会变差。
文档解析后,不能简单按固定字符数硬切。更好的方式是按标题层级、段落、章节、表格整体、问答对、业务规则块切分,并用滑动窗口补充上下文。
企业知识库可以优先考虑父子切分 Small-to-Big:
text
小 Chunk:用于向量检索,提高召回精度;
大 Chunk / 父文档:用于回答生成,保证上下文完整。
比如检索时用"某一条报销规则"命中,生成答案时带上"这一整节差旅报销制度"。
企业级知识库不要只依赖向量数据库。比较稳的做法是:
text
向量库 + 全文索引 + 元数据数据库 + 可选知识图谱。
向量库负责语义检索,适合处理同义表达、自然语言问答、模糊查询、跨表述检索等问题。常见向量数据库包括 Milvus、Faiss、Qdrant、Weaviate、Pinecone、pgvector、Elasticsearch vector search。
全文索引负责关键词精确匹配,比如制度编号、合同编号、产品型号、人名、部门名、专有名词、错误码、报销单号、API 字段名等。这类内容用 Elasticsearch、OpenSearch 或 Lucene 更稳。
元数据非常关键,每个 Chunk 都应该带:
text
doc_id;
chunk_id;
source_type;
source_url;
file_name;
title_path;
department;
owner;
created_at;
updated_at;
version;
permission_group;
page_number;
section_title;
effective_date;
expired_date。
这些元数据可以用于权限过滤、版本控制、引用溯源、时间过滤、部门过滤、文档更新和问题排查。
知识图谱不是必须,但在组织架构关系、产品依赖关系、客户-合同-项目关系、员工-部门-权限关系、制度引用关系、多跳关系推理中很有价值。
在线问答的核心流程是:
text
问题理解 → 检索 → 精排 → 组装 Prompt → 大模型生成 → 引用溯源
用户提问后,不能所有问题都直接查知识库。要先做意图识别和路由,判断是闲聊、知识库问答、数据查询、操作类任务、多步任务,还是需要用户补充信息。
对于知识库问答,会走混合检索,也就是向量检索 + 关键词检索 + 元数据过滤。检索前或检索中必须做 ACL 权限过滤,确保用户只能看到自己有权限的数据。权限控制不能交给模型,必须由工程系统兜底。
召回候选文档后,要做 Rerank 精排。典型流程是先召回 Top 50 / Top 100,再用 Cross-Encoder 或 Reranker 精排,最后取 Top 5 / Top 10 进入 Prompt。
Prompt 组装时,不要简单把所有检索结果拼进去,而要结构化组织。可以包括用户问题、用户角色与权限范围、证据片段、文档标题、页码或章节、更新时间、引用 ID、回答要求和无法回答时的规则。比如要求模型只能基于给定上下文回答,没有依据就明确说未找到,不要编造制度条款。
企业级系统必须解决权限隔离、防幻觉、引用溯源、审计日志和反馈闭环。
权限控制包括:
text
文档级权限;
段落级权限;
字段级权限;
部门级隔离;
租户级隔离;
敏感信息脱敏;
审计日志。
防幻觉可以从这些方面做:
text
只允许基于检索上下文回答;
没有证据时明确说不知道;
强制输出引用来源;
对高风险问题增加二次校验;
对制度、财务、法务类问题降低自由发挥空间。
反馈闭环包括用户点赞点踩、追问、问题是否被解决、无答案问题、低置信度问题、高频搜索词、检索无结果 Query、人工纠错数据、客服转人工记录等。这些数据可以反向优化 Chunking、Embedding、关键词词典、Rerank 模型、Prompt 模板、知识库内容和权限配置。
架构可以这样理解:

普通 RAG 解决的是"能不能答",企业级知识库 Agent 解决的是"能不能稳定、准确、安全、可追溯地答"。
单纯用向量检索 RAG 做长期记忆,会遇到什么问题?
如果单纯用向量检索做长期记忆,最大的问题是:向量空间只擅长表达语义相似,但不天然理解时间顺序、上下文层级、事实新旧、关键词精确匹配和长期记忆演化。
所以长期记忆不能只靠向量相似度,而应该结合:
text
向量检索;
关键词检索;
元数据过滤;
时间衰减;
摘要索引;
Rerank 重排序。
第一个问题是时序错乱。向量没有天然时间轴,容易把很久以前的旧记忆召回出来,导致旧偏好覆盖新偏好。
例如用户之前说过:
text
2024 年:我喜欢简洁风格;
2025 年:我现在更喜欢高级、神圣、温暖的视觉风格。
如果用户后来问"我现在更偏好的 UI 风格是什么?",单纯向量检索可能把 2024 年那条旧记忆召回来,因为它和"UI 风格"语义相似。
第二个问题是语境碎片化。长期记忆通常来自多轮对话、文档、工具结果和历史行为。如果切分过细,模型可能只看到局部片段,看不到完整背景和决策过程。
第三个问题是语义漂移。用户问题往往很短,但记忆内容可能很具体,短 Query 和长 Chunk 在向量空间里不一定足够接近,容易漏召回关键记忆。尤其是人名、编号、版本号、错误码、制度编号、专有名词等精确信息,单靠向量检索并不稳定。
索引构建阶段,长期记忆不能只存原始 Chunk,还应该存摘要。可以设计两层:
text
Parent:会话摘要 / 任务摘要 / 文档摘要;
Child:原始片段 / Chunk / 证据细节。
检索时先命中 Parent Summary,再根据 Parent 找到相关 Child Chunks。这就是 Small-to-Big 思路:先用小而精的摘要快速定位主题,再回到具体片段拿证据。
长期记忆还必须处理新旧关系。每条记忆应该带时间字段,比如:
text
created_at;
updated_at;
last_used_at;
effective_from;
effective_to;
version。
排序时可以综合语义相似度、时间新鲜度和重要性:
text
最终得分 = 语义相似度得分 + 时间新鲜度得分 + 重要性得分
每条记忆还应该带元数据:
text
memory_id;
user_id;
project_id;
topic;
source;
created_at;
updated_at;
memory_type;
importance;
confidence;
status;
version;
permission_scope。
其中 memory_type 可以区分用户偏好、项目背景、最终决策、中间讨论、技术知识、工具结果、文件内容、任务状态。最终决策和中间讨论的权重不应该一样。
检索阶段,优化检索质量的第一步是不要只用向量检索,而是使用混合检索:
text
向量检索:解决语义相似;
关键词检索:解决精确匹配;
元数据过滤:解决时间、范围、权限、类型约束。
用户问题经常很短、很模糊,不适合直接检索。这时可以做 Query Rewrite,把问题改写成更适合检索的形式。比如"之前那个被拒问题呢?"可以改写成"App Store 审核被拒原因、Guideline 2.1、Guideline 5.1.1、Sign in with Apple、IAP 强制注册、隐私披露"。
对于更抽象的问题,还可以用 HyDE:先让模型生成一个假设性答案,再用这个假设答案去做向量检索。它适合用户提问很短、问题很抽象、原始 Query 语义不足、文档表达和用户表达差异较大的场景。
复杂问题还可以多路召回,比如同时召回语义相似结果、关键词匹配结果、最近时间结果、高重要性记忆、同一项目下的最终决策、同一主题下的摘要,再合并去重后进入 Rerank。
向量检索适合粗召回,不适合直接决定最终上下文。更稳的流程是:
text
Top 50 粗召回
↓
Rerank 精排
↓
Top 5 / Top 10 进入 Prompt
长期记忆中经常会有重复信息,需要语义去重、同主题合并、同版本保留最新、同来源保留权威版本,并标记冲突记忆。
冲突处理也很重要。比如早期用户想要黑金风格,后期又明确不要黑色背景。系统不能把两条平等地塞给模型,而要根据时间新旧、用户是否明确确认、是否为最终决策、来源可信度和记忆类型判断权重。
最后还要做上下文压缩,只保留和问题相关的句子、关键事实、引用来源、时间和版本信息。
完整链路可以这样设计:
text
用户问题
↓
意图识别
↓
Query Rewrite / HyDE
↓
元数据过滤
↓
多路召回
- 向量检索
- 关键词检索
- 摘要索引检索
- 最近记忆检索
↓
合并去重
↓
Rerank 重排序
↓
时间衰减 / 重要性加权
↓
冲突检测
↓
上下文压缩
↓
组装 Prompt
↓
LLM 生成答案
评估长期记忆检索质量,不能只看"有没有搜到",还要看:
text
Recall@K;
Precision@K;
MRR;
NDCG;
Answer Groundedness;
Freshness Accuracy。

LambdaMART 是什么?在 RAG 系统中通常用在哪个环节?
LambdaMART 是一种 Learning to Rank 排序算法,通常用在 RAG 系统的 Rerank 精排环节。它不负责生成答案,也不是 Embedding 模型,更不是向量数据库。它的作用是在粗召回之后,对候选文档重新打分和排序,把最可能帮助 LLM 回答问题的文档排到最前面。
典型链路是:
text
用户问题
↓
粗召回 Retrieval
↓
候选文档 Top 50 / Top 100
↓
LambdaMART / Cross-Encoder / Reranker 精排
↓
Top 5 / Top 10 高质量文档
↓
组装 Prompt
↓
LLM 生成答案
粗召回阶段追求召回率,宁可多召回一点,也不要漏掉关键文档。精排阶段追求排序质量,目标是把最有可能支撑答案的文档排到前面,减少 LLM 上下文噪声。
LambdaMART 可以拆成 Lambda + MART。
MART 可以理解为基于树模型的梯度提升方法,类似 GBDT。它不是深度神经网络,而是通过很多棵决策树组合起来,对样本进行打分。
Lambda 可以理解为排序梯度。排序问题和普通分类、回归不一样。普通分类关心单个样本是不是正例,排序关心的是这个文档应该排在另一个文档前面还是后面,以及整个列表排序质量好不好。
LambdaMART 通常优化 NDCG、MAP、MRR 这类排序指标,所以它更关注整个候选列表的排序质量,而不是单个文档的孤立分类。
向量检索主要看语义相似度,但真实 RAG 中,一个文档是否应该排在前面,不只取决于语义相似。
比如两个文档都和问题语义相关:
text
文档 A:内容相关,但三年前的旧制度;
文档 B:内容相关,是上个月更新的正式制度。
业务上应该优先文档 B。
LambdaMART 的价值就在于它可以融合更多特征:
text
语义相似度;
BM25 分数;
标题命中;
关键词命中;
文档更新时间;
文档权威度;
文档点击率;
用户反馈;
文档类型;
权限范围;
业务优先级。
BM25 是传统关键词排序算法,速度快,适合关键词、编号和专有名词,但不理解语义。
Cross-Encoder 是神经网络精排模型,会把 Query 和 Document 拼在一起输入模型,语义理解强,但计算成本高、速度慢,不适合大规模候选,通常只适合 Top 20 / Top 50 精排。
LambdaMART 是基于树模型的排序算法,推理速度快,容易融合业务特征,适合工业级高并发系统,可解释性也相对更好。但它需要训练数据,也依赖特征工程,单独使用时语义理解不如 Cross-Encoder。
真实系统里三者可以组合:
text
BM25 + Vector 召回
↓
Cross-Encoder 计算语义相关性分数
↓
LambdaMART 融合语义分数 + 业务特征
↓
输出最终排序
LambdaMART 在 RAG 里可以融合五类特征:
text
文本匹配特征:BM25、TF-IDF、关键词命中、标题命中、正文命中、Query 覆盖率;
语义相关特征:向量相似度、Embedding 距离、Cross-Encoder 分数、HyDE 分数;
文档质量特征:权威度、是否正式发布、是否草稿、是否过期、更新时间、版本号、来源可信度;
用户行为特征:点击率、点赞率、点踩率、收藏率、历史采纳率、人工标注相关性;
业务上下文特征:用户部门、用户角色、权限范围、项目匹配度、地域匹配度、时间有效性、业务优先级。
所以,LambdaMART 在 RAG 中不是用来"找文档",而是用来"重排文档"。它通常站在粗召回和 LLM 生成之间,决定哪些文档最值得进入最终上下文。
Function Calling 的底层逻辑是什么?
Function Calling 不是大模型自己执行代码。大模型本身不会真的访问数据库、调用 API 或执行 Python 函数。它做的是根据开发者提供的工具说明,判断该调用哪个函数,并生成结构化参数。真正执行函数的是外部程序或业务后端。
整个流程是:
text
开发者注册工具 Schema
↓
用户提出问题
↓
模型判断是否需要工具
↓
模型输出 tool_call
↓
外部程序执行函数
↓
工具结果回传模型
↓
模型生成最终回答
开发者会先告诉模型有哪些工具可以用、每个工具叫什么、能做什么、需要哪些参数、参数类型是什么、哪些参数必填。比如:
json
{
"name": "query_order",
"description": "查询用户订单信息",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单编号"
}
},
"required": ["order_id"]
}
}
用户问"帮我查一下订单 20240518 的物流状态",模型会判断这不是普通知识问答,而是需要查真实订单,于是输出类似:
json
{
"tool_name": "query_order",
"arguments": {
"order_id": "20240518"
}
}
真正执行的是业务系统:
python
result = query_order(order_id="20240518")
工具结果再回传给模型,模型基于结果生成自然语言回答。
Function Calling 的底层稳定性主要依赖几件事:
text
工具协议与 SFT 微调:模型通过训练学会什么时候调用工具、调用哪个工具、参数怎么填;
特殊标识符 Tokens:区分普通文本和工具调用;
约束解码 Constrained Decoding:限制模型生成符合 Schema 的内容;
后端校验:对模型输出做强类型校验和权限校验。
所以 Function Calling 不是"模型真的会调用函数",而是模型生成结构化调用请求,工程系统负责真正执行。
生产环境里还会遇到几个问题。
第一是参数幻觉。模型可能生成 API 不存在的参数,或者在不该调用工具时强行调用。解决方式是参数 Schema 严格校验、工具列表动态裁剪、Few-shot 示例、只暴露当前任务相关工具,非法参数直接拒绝或让模型重试。
第二是格式不稳定。模型输出的 JSON 可能格式不合法,导致后端反序列化失败。解决方式是启用 Strict Mode,使用 JSON Schema、Pydantic、Zod 做强类型校验,失败后把错误信息回传给模型修复,并设置最大重试次数。
第三是工具误调用。模型可能在不需要工具时调用工具,或者选错工具。解决方式是工具描述写清楚,工具按场景动态注入,只给模型当前必要的 3 到 5 个工具,高风险工具需要二次确认。
第四是安全越权。用户可能通过 Prompt Injection 诱导模型调用高危工具。解决方式是模型不直接拥有执行权限,后端做权限校验,高危工具加人工确认,敏感操作加审批流,记录审计日志,并在沙盒中执行。
用传统工程类比:
text
tools 定义 ≈ 接口文档 / OpenAPI / Retrofit interface;
tool_call ≈ 一次 RPC 调用请求;
arguments ≈ 请求参数 DTO;
本地执行函数 ≈ Service / Repository / DAO 调用;
tool result ≈ 接口返回 Response;
Schema 校验 ≈ Bean Validation / 参数校验;
权限控制 ≈ RBAC / 鉴权拦截器。
Function Calling 的本质是:模型负责理解意图和生成结构化调用请求,工程系统负责真正执行函数和保证安全边界。
上百个 Skill 塞爆 System Prompt 时,如何重构?
这个问题不是问怎么把 Prompt 写短一点,而是在问如何把 Prompt 工程升级成 Agent 技能管理架构。
当上百个 Skill 塞爆 System Prompt 时,不应该继续堆 Prompt,而应该把 Skill 从静态 Prompt 中拆出来,做成一套按需加载、动态路由、分层管理、可缓存、可训练内化的技能管理系统。
核心思路是从"全量加载 Skill"变成"按任务动态选择 Skill"。
text
用户问题
↓
意图路由
↓
Skill RAG 检索
↓
动态组装 Prompt
↓
必要时分身 Agent 协作
↓
底层缓存 / SFT 优化
不能全量塞进 Prompt,主要有三个原因。
第一是 Token 成本。每个 Skill 都可能包含名称、功能说明、参数格式、调用条件、输入输出示例、异常处理、安全约束。上百个 Skill 加起来会很长,每次请求都携带这些内容会导致成本高、首字延迟变慢。
第二是 Lost in the Middle。Prompt 太长时,中间的规则容易被模型忽略。明明写了某个规则,模型却没遵守;明明提供了正确 Skill,模型却没有调用。
第三是注意力污染。无关 Skill 会干扰模型判断,导致选错工具、误调用工具,或者把多个 Skill 的规则混在一起。
可以按五层进行重构。
第一层是 Intent Gateway 意图路由。用户请求进来后,先用轻量模型、规则、关键词、传统分类模型或 Embedding 相似度做任务分类。判断它是搜索、数据分析、代码执行、文档总结、图片处理、多步规划还是普通闲聊。这层只输出 Skill 大类,不直接处理所有任务细节。
第二层是 Skill RAG 技能检索。把每个 Skill 文档化,包括:
text
Skill 名称;
Skill 描述;
适用场景;
不适用场景;
输入参数 Schema;
输出格式;
调用示例;
边界条件;
失败处理;
安全要求。
然后存到向量数据库、全文索引、Hybrid Search、标签索引或规则路由表里。用户问题来了以后,只召回最相关的 Top-K Skill,比如 3 到 5 个。
第三层是 Prompt 动态组装。系统根据路由结果和 Skill 检索结果动态拼接 Prompt。Prompt 里只放基础系统规则、当前任务、相关 Skill、参数 Schema、动态 Few-shot 和输出约束。
第四层是分身代理架构。如果任务复杂度继续上升,可以引入多 Agent 分治。Manager Agent 只负责任务理解、拆解、分配和汇总;Data Agent 只加载 SQL、Python、表格分析、可视化相关 Skill;Search Agent 只加载网页搜索、资料检索、引用整理相关 Skill;Writing Agent 只加载写作、润色、结构化表达相关 Skill;Code Agent 只加载代码搜索、修改、测试执行相关 Skill。
第五层是底层优化。稳定、高频、重复的内容可以用 Context Caching 缓存,比如基础系统规则、通用安全规范、高频 Skill 固定说明、通用输出格式。这样可以降低重复计算成本和首字延迟。对于调用频率极高、格式固定、规则稳定的 Skill,可以考虑 SFT 或偏好训练,让模型把调用模式内化,减少对长 Prompt 的依赖。
这种架构不是没有代价。路由层会增加一次分类开销,Skill RAG 会增加一次检索开销,多 Agent 会增加网络交互和状态同步复杂度,动态 Prompt 组装也需要更强的工程治理。
比较实际的落地路径是:
text
先做 Intent Gateway + Skill RAG;
再做 Prompt 动态组装;
复杂任务再引入 Specialist Agent;
高频稳定场景再做 Cache 和 SFT。
JSON + LaTeX 混合生成为什么容易崩?如何稳定?
当大模型同时生成 JSON 和 LaTeX 时,最容易出问题的是转义冲突。
LaTeX 里大量使用反斜杠,比如:
latex
\alpha + \beta
但 JSON 字符串里,反斜杠本身也是转义字符,所以它必须写成:
json
{
"formula": "\\alpha + \\beta"
}
如果模型只输出单反斜杠:
json
{
"formula": "\alpha + \beta"
}
后端 json.loads() 很可能直接报错,或者解析结果不符合预期。
这个问题难处理,主要有三个原因。
第一,Token 化会切碎反斜杠。模型在长文本生成时,要同时维持 JSON 结构合法、字符串引号闭合、LaTeX 公式正确、反斜杠数量正确、嵌套层级正确。
第二,预训练里 LaTeX 通常是原始形态,比如 \frac{a}{b},而不是 JSON 转义后的 "\\frac{a}{b}"。所以模型容易按照更熟悉的 LaTeX 原始形式输出。
第三,公式越复杂,转义越容易错。比如矩阵、分段函数、多行推导里会同时出现反斜杠、换行、花括号、引号等特殊字符。
更稳的方案是分层处理。
第一层,优先使用 Structured Output、JSON Mode、Function Calling、Tool Calling 或 Schema 约束,让模型不是自由生成文本,而是在结构化约束下生成。
第二层,如果业务不强制要求 JSON,可以格式降维,改用 YAML 多行字符串或 Markdown 代码块。它们对公式、多行文本和推导过程更友好。
比如 Markdown 可以这样写:
markdown
公式:
```latex
\alpha + \beta
```
YAML 可以这样写:
yaml
formula: |
\alpha + \beta
第三层,如果必须使用 JSON,而且容错率要求很高,可以把 LaTeX 内容做 Base64 编码后再放入 JSON,实现特殊字符物理隔离。优点是特殊字符安全、JSON 解析稳定,适合医疗、金融、考试系统、合同系统、自动判题、结构化题库等低容错场景。
第四层,在 Prompt 里加入 Few-shot 示例,明确告诉模型 JSON 中的 LaTeX 必须双反斜杠转义。例如:
json
{
"formula": "\\frac{a}{b} + \\sqrt{x}",
"description": "这是一个 LaTeX 公式,所有反斜杠都已经双重转义。"
}
第五层,后端做工程兜底,包括 json.loads、JSON Schema 校验、字段类型校验、LaTeX 字符串检查、json_repair、自动补转义、失败重试、异常日志和人工降级。
所以,这个问题不能只靠 Prompt 里提醒模型"多写一个反斜杠"。它本质不是让模型学会"数斜杠",而是通过架构设计消灭斜杠带来的不确定性。
vLLM、SGLang、TensorRT-LLM 怎么选?
这不是一个"哪个框架绝对性能最好"的问题,而是要根据业务阶段、硬件环境、吞吐目标、延迟要求、上下文复用模式和工程维护成本做取舍。
可以先给出一个简单判断:
text
初创团队 / 快速验证 / 模型适配优先:首选 vLLM;
复杂 Agent 工作流 / 多轮对话 / 长 Prompt 复用:优先 SGLang;
超大规模商用部署 / NVIDIA GPU 集群 / 极致吞吐和低延迟:选择 TensorRT-LLM。
vLLM 可以理解为开源大模型推理服务的通用底座。它的优势是模型适配广、社区生态成熟、部署相对简单、OpenAI API 兼容较好,适合快速上线、通用聊天、问答、RAG 和简单 Agent。
vLLM 的核心技术是 PagedAttention。大模型推理时,每个请求都会产生 KV Cache,长上下文和多并发请求下显存管理会成为瓶颈。PagedAttention 类似操作系统的分页内存管理,把 KV Cache 分成一块块 page,不要求连续分配,从而提升显存利用率,缓解显存碎片问题。
SGLang 更适合复杂 Agent 工作流,尤其适合多轮对话、复杂 Prompt、前缀复用、结构化输出、函数调用、多步骤推理流程。
它的核心优化之一是 RadixAttention。很多 Agent 请求之间有大量相同前缀,比如固定 System Prompt、工具说明、安全规则、输出格式、Few-shot 示例。如果每个请求都重新计算这些前缀,会非常浪费。RadixAttention 可以复用相同 Prompt 前缀的 KV Cache,减少重复计算。
TensorRT-LLM 是 NVIDIA 生态里的高性能大模型推理框架,目标是在 NVIDIA GPU 上尽可能压榨吞吐和延迟性能。它的优势来自 Kernel Fusion、图优化、Tensor Parallel、Pipeline Parallel、FP8 / INT8 / INT4 量化、KV Cache 优化和 NVIDIA GPU 专项加速。
它适合请求量极高、硬件锁定 NVIDIA、模型版本相对固定、对 TTFT / TPOT / 吞吐 / 单位成本有极致要求,并且有专业推理优化团队的场景。
三者可以这样对比:
| 框架 | 核心优势 | 典型技术 | 适合场景 | 主要代价 |
|---|---|---|---|---|
| vLLM | 通用、生态成熟、部署快 | PagedAttention | 项目初期、通用推理、RAG、快速验证 | 极致 Agent 前缀复用和硬件压榨不是最强 |
| SGLang | Agent 工作流强、前缀复用强 | RadixAttention | 多轮 Agent、长 Prompt、Few-shot、结构化输出 | 生态和通用基座属性相对 vLLM 更聚焦 |
| TensorRT-LLM | 极致性能、硬件级优化 | Kernel Fusion、图优化、量化 | NVIDIA 集群、大规模商用、极致吞吐低延迟 | 工程复杂、硬件绑定强、调优成本高 |
真实工程里,更推荐渐进式选型:
text
阶段一:vLLM 跑通业务;
阶段二:SGLang 优化复杂 Agent;
阶段三:TensorRT-LLM 压榨极致性能。
评估时要看:
text
TTFT;
TPOT;
Throughput;
QPS;
并发能力;
GPU 利用率;
显存占用;
KV Cache 命中率;
单位 Token 成本;
P95 / P99 延迟;
模型适配成本;
运维复杂度。
Agent 架构中的 Skill 是什么?渐进式披露如何实现?
Skill 不是简单的工具。更准确地说,Skill 是一个高内聚的能力包,不只是一个 API。它把工具、领域知识、操作步骤、约束规则、Few-shot 示例和失败处理经验封装在一起。
可以这样理解:
text
Skill = Tool API + 使用说明 + 领域知识 + 约束规则 + 示例 + 失败处理策略
它不是简单的函数接口,而是一个可被 Agent 按需检索、动态加载、执行和卸载的能力模块。
Tool 更像接口,比如搜索网页、查询数据库、执行 Python、读取文件、发送邮件、生成图片、调用 HTTP API。它关注工具叫什么、能做什么、需要哪些参数、返回什么结果。
Skill 更像能力包。比如"企业知识库问答 Skill"不只是一个检索 API,它还应该包含什么时候查知识库、如何做 Query Rewrite、如何选择向量检索还是关键词检索、如何判断召回结果是否足够、如何组织引用来源、没有答案时如何回答、哪些内容不能编造。
所以可以这样理解:
text
Tool 解决"能不能做";
Skill 解决"怎么正确地做"。
一个完整 Skill 通常包含:
text
1. Skill 名称
2. Skill 描述
3. 适用场景
4. 不适用场景
5. 可调用工具列表
6. 参数 Schema
7. 操作步骤 SOP
8. 输入输出格式
9. 成功示例
10. 失败示例
11. 异常处理策略
12. 安全边界
13. 权限要求
14. 评价标准
渐进式披露就是:不要一开始把所有 Skill 细节都塞进 System Prompt,而是先给模型一个极简技能目录。当模型判断当前任务需要某个 Skill 时,再动态加载这个 Skill 的完整说明、参数、示例和约束。
第一层是发现层 Metadata。只给模型一个极简目录,让它知道系统大概有哪些能力,比如搜索类、数据分析类、文档处理类、代码类、图片类、邮件类。
第二层是决策层 Reasoning。当用户提出任务后,模型或路由器判断需要哪个 Skill。可以用规则路由、轻量分类模型、Embedding 检索、LLM 路由判断或 Skill RAG。
第三层是注入层 Injection。确定需要哪些 Skill 后,系统动态读取对应 Skill 的完整说明文档,比如 skills/table_analysis/skill.md,再把参数、步骤、示例和约束拼到当前上下文中。
完整运行闭环是:
text
用户请求
↓
加载极简 Skill 目录
↓
意图识别 / Skill 路由
↓
检索最相关 Skill
↓
动态加载 Skill 详细说明
↓
组装当前 Prompt
↓
模型调用工具或执行任务
↓
任务状态更新
↓
不再需要的 Skill 自动卸载
这就是渐进式披露的核心:把模型从"死记硬背所有 Skill",变成"需要时查阅相关 Skill"。
工程落地里还有三个难点。
第一,检索迷失和死循环。系统可能找不到正确 Skill、加载错误 Skill,或者反复检索 Skill 但不执行任务。解决方式是先做大类路由,再做小类 Skill 检索,给 Skill 增加清晰 metadata,使用关键词 + 向量混合检索,并限制最大检索次数。
第二,上下文爆炸残留。连续加载多个 Skill 后,历史上下文会越来越大。解决方式是 Auto-unloading 自动卸载,任务阶段切换时清理旧 Skill,只保留 Skill 执行结果,不保留完整说明。
第三,生态和标准化。不同 Agent 框架里的 Skill 格式可能不一样,所以要统一 Skill 元数据规范、参数 Schema、权限声明、执行日志、错误码,必要时接入 MCP 等开放协议。
Skill、MCP、Tool Calling 的关系可以这样理解:
text
Tool Calling:让模型调用一个具体工具;
MCP:让工具和上下文能力通过统一协议暴露;
Skill:把工具、知识、流程、示例和约束封装成高层能力包。
也就是说:
text
Tool 是底层动作;
MCP 是连接协议;
Skill 是能力封装;
Agent 是调度者。
这些 Agent 架构问题之间有什么统一逻辑?
这些问题看起来很多,但它们其实都在围绕同一条主线:
text
如何把大模型从一个"生成文本的模型",升级成一个"稳定执行任务的工程系统"。
可以这样串起来:
text
Function Calling:让模型具备调用外部工具的能力;
ReAct:让模型围绕工具形成推理-行动-观察闭环;
上下文取舍:让模型在有限 Token 内看到最重要的信息;
RAG:让模型从外部知识库中获取可靠依据;
LambdaMART / Rerank:让进入 Prompt 的知识更精准;
长期记忆优化:让 Agent 能记住最新、相关、可靠的信息;
Skill:把工具、知识、流程和经验封装成能力模块;
渐进式披露:让 Skill 按需加载,而不是全塞进 Prompt;
System Prompt 重构:把大 Prompt 改造成动态能力调度系统;
JSON + LaTeX 稳定生成:用协议和工程兜底解决结构化输出风险;
vLLM / SGLang / TensorRT-LLM:根据业务阶段选择推理基础设施;
多 Agent 结构化通信:用状态、Schema 和校验保证协作稳定。
最终可以总结成一句:
text
Agent 工程的核心,不是让模型"知道更多",而是让模型在正确的时间拿到正确的上下文、调用正确的工具、输出可校验的结果,并在失败时能够恢复。
分析 Agent 架构类问题时,可以按照这个结构展开:
text
1. 先定义本质
这不是一个 Prompt 问题,而是一个工程链路问题。
2. 再给出分层架构
模型层、工程层、架构层、评估层。
3. 讲清楚关键流程
路由、检索、Rerank、Prompt 组装、工具调用、校验、反馈。
4. 主动讲风险
幻觉、格式错误、权限越权、上下文爆炸、循环失败、过期信息、噪声污染。
5. 给出兜底策略
Schema 校验、Constrained Decoding、重试、降级、人工审核、审计日志。
6. 最后讲评估指标
成功率、Token 成本、TTFT、P95/P99、Recall@K、Precision@K、NDCG、Groundedness。
一句话概括就是:
text
真正的 Agent 架构,不是把更多工具和更多 Prompt 塞给模型,而是通过路由、检索、约束、校验、缓存、分治和反馈闭环,让模型在受控边界内完成任务。
真正成熟的 Agent 架构,是在模型智能和工程约束之间找到平衡:
text
模型负责理解、推理、决策和生成;
工程系统负责工具执行、权限控制、格式校验、状态管理、异常恢复和效果评估。








