LlamaIndex:响应合成、对话与本地推理
「本系列第 14/17 章」。基线:LlamaIndex
0.14.23。检索只给出候选;真正决定延迟、费用和保真度的,是合成模式、对话是否改写追问,以及 LLM 跑在哪一个进程里。本章把这三件事放在同一张地图上,避免“检索没问题但答案又慢又飘”。慢通常来自多次 LLM 调用,飘通常来自改写、截断或本地模板与客户端元数据不一致。
1. 合成:调用次数按 repack 后的块数算
BaseSynthesizer 接收 QueryBundle 与 NodeWithScore[],按 MetadataMode.LLM 抽出文本(或内容块),再调用 LLM。工厂默认模式是 compact。下面三个常用模式的调用次数,都以 PromptHelper.repack 之后的 chunk 数 K 为准,不是 similarity_top_k,也不是原始检索条数。repack 可能合并小块,也可能因窗口不够再切开,所以 K 是合成器内部的工作单位。
refine。 第一块走 QA prompt,之后每一块带着 existing_answer 再 refine。K 块就是 1 次 QA 加 K−1 次 refine。existing answer 变长,还可能把当前块再拆开压回队列,实际次数只多不少。流式只能看到最后一次有效 refine:前面各步必须先完整生成,后续 prompt 才有完整旧答案。它适合“希望后到的证据能修正前面结论”,代价是延迟随 K 线性上升。
compact(CompactAndRefine)。 先尽量把小块拼进上下文窗口,再完全复用 Refine。K=1 时才是单次 LLM 调用;K>1 时仍是 QA + 多次 refine。不要把它写成“永远一次调用”。常规问答优先选它,是因为它通常能把 K 压小,而不是因为它取消了 refine 算法。窗口估算来自 LLM metadata 的 context_window 与预留输出长度;客户端数字和服务端真实 KV 不一致时,你会看到莫名其妙的多次 refine 或截断。
tree_summarize。 每一层先 repack:只剩 1 块就出根答案;仍有多块则每块先摘要,再把摘要递归送回。同步默认按块串行,use_async=True 时同层可并行。总调用次数是各层块数之和,不是“一层算完”。中间层会压缩细节,适合全局摘要,不适合“逐证据保真”的问答。流式也只发生在收敛到根的那一次。
对照记忆,避免把名字望文生义:simple_summarize 拼接后截断、恰好一次调用,超长尾部会丢;accumulate 每块独立回答再编号拼接,不支持 streaming,运行时会抛错;compact_accumulate 先全局 repack 再 accumulate,次数更少,但仍是互不相融的多答案;generation 丢掉上下文只按问题生成,却仍保留传入的 source_nodes,那些来源不能当成依据;no_text / context_only 不调 LLM,前者答案为空、节点在 source_nodes,后者直接拼接上下文便于检查注入文本。空检索时除 generation 外直接返回空响应,不会“为了礼貌再问一次模型”。
选型可以记成课堂口诀:常规 RAG 用 compact;要覆盖全部长上下文且接受多次调用,用 refine;只要文档级摘要,用 tree_summarize;只要看模型实际吃到什么,用 context_only。source_nodes 始终是输入候选记录,不保证每段都进入了最终 prompt。
2. 对话:改写、检索与记忆分开
index.as_chat_engine() 在 0.14.23 仍可用的模式是:
ChatMode |
实际类 | 要点 |
|---|---|---|
simple |
SimpleChatEngine |
不检索,只是带历史聊天 |
condense_question |
CondenseQuestionChatEngine |
先改写,再整链 QueryEngine |
context |
ContextChatEngine |
用本轮原文检索,再带历史合成 |
condense_plus_context / best |
CondensePlusContextChatEngine |
改写用于检索,合成仍用原问题 |
best 现在只是 condense_plus_context 的别名,不再“自动挑选 Agent”。旧文档里“best 会在 ReAct 和 OpenAI Agent 之间选择”已经过期。ChatMode.REACT / ChatMode.OPENAI 已删除,调用立即 ValueError。需要工具时,下一章的 ReActAgent + AgentWorkflow 才是正路。
as_chat_engine() 会先解析 LLM,并且无条件构造一次 as_query_engine;context 与 condense_plus_context 随后还要 as_retriever。传入的关键字必须同时能被这些工厂消化,错误参数可能在模式分派前就失败。
有历史时,condense_plus_context 会先 llm.complete() 改写追问,用改写结果检索,再用原始用户句子做合成。所以一次用户消息可能对应两次 LLM 调用,加上 compact/refine 自身的 K 次,延迟要从这条加法看,不能只看生成阶段的流式输出。首轮、空历史或 skip_condense=True 才省掉改写。context 模式不改写,适合短追问仍带得动原词的场景;代词很多时,不改写会检索漂移。
记忆请用:
1 | from llama_index.core.memory import Memory |
ChatMemoryBuffer 仍能导入,但类注释已标记 deprecated。Memory 负责“给模型看多长的历史”,chat store 负责“消息按 key 存在哪”。ChatEngine 实例持有 Memory,不要做成全站单例;每个会话一份 Memory,或独立的 chat-store key。chat_history= 是整表替换,不是追加。reset() 清空该 Memory 对应的历史,共享 key 时会互相影响。流式对象背后还有写回历史的后台任务,生成器要消费完,过早断开可能丢掉末尾并影响历史。
system_prompt 用来约束“只根据检索资料回答”;它与某些引擎的 prefix_messages 互斥,不要两个一起塞。source_nodes 仍然只是检索结果,多轮之后尤其不能当成逐句引用。
3. 本地推理:只走 llama-server 的 OpenAI HTTP
本系列推荐的本地拓扑只有一条。LlamaIndex 不读 GGUF,也不管理推理进程;模型路径、GPU offload、槽位和 chat template 都在 llama-server 一侧。
1 | llama-server :8080 → OpenAILike → chat / 合成 / 追问改写 |
进程内的 llama_index.llms.llama_cpp.LlamaCPP 是另一条部署,不要和 HTTP 客户端混在同一套 Settings 里。混用的典型后果是:嵌入走服务、生成走进程内,上下文窗口、模板和并发模型对不上,出了错两边日志都只看到一半。
1 | from llama_index.llms.openai_like import OpenAILike |
这些类来自独立集成包,需要另行安装。api_base 要带服务实际支持的 /v1。本地服务不校验密钥时,仍须给非空占位,因为 OpenAI 客户端要求有 key。is_chat_model=True 才走 chat-completions;否则消息会被拼成 completion prompt,输出会像在回显模板。Embedding 的构造参数是 model_name,不是 model。模型 ID 以 curl http://127.0.0.1:8080/v1/models 的返回为准,不要默认等于 GGUF 文件名。
is_function_calling_model=False 时,Agent 应走 ReActAgent,不要假定 llama-server 的 chat-completions 等于可靠的 OpenAI tools。context_window 只是客户端预算,必须和 server 的 -c 对齐;它不会把服务端 KV cache 变大。换 embedding 模型或维数后,必须重建索引。simple store 持久化后,加载不会向 :8081 重算文档向量,但每次查询仍要嵌入当前问题。
流式只缩短生成阶段的可见等待。embedding、向量检索、追问改写都发生在首个 token 之前。把“开了 streaming”理解成“整条 RAG 都在流”,会误判首字延迟。
把本章三件事连成一句课堂结论:先选定合成模式并按 K 估调用次数,再决定对话要不要改写追问,最后只通过 :8080 / :8081 访问本地模型。任意两件事同时改,你将无法解释延迟来自 refine、来自 condensing,还是来自 embedding 排队。
课堂练习可以很小。同一批 source_nodes,分别开 context_only、compact、tree_summarize,只比较调用次数和答案是否丢掉约束条件;不要同时改 top-k。再拿一句带代词的追问,对比 context 与 condense_plus_context 实际拿去检索的字符串。本地路径上,把 is_chat_model 故意关掉一次,看输出是否变成模板回显——这能帮你记住 chat completions 与 completion prompt 不是一回事。
下一章补齐 Agent、属性图,以及持久化、多租户这类工程约束。本地模型只要还把 function calling 关掉,工具循环就按 ReAct 来读;不要指望把 ChatEngine 的模式拧到已经删除的 REACT 上会自动出现 Agent。对话引擎管“带着历史怎么检索和合成”,Agent 管“要不要调用工具”,二者不要混成一个开关。
正在加载留言…