LlamaIndex:响应合成、对话与本地推理

LlamaIndex:响应合成、对话与本地推理

「本系列第 14/17 章」。基线:LlamaIndex 0.14.23。检索只给出候选;真正决定延迟、费用和保真度的,是合成模式、对话是否改写追问,以及 LLM 跑在哪一个进程里。本章把这三件事放在同一张地图上,避免“检索没问题但答案又慢又飘”。慢通常来自多次 LLM 调用,飘通常来自改写、截断或本地模板与客户端元数据不一致。

1. 合成:调用次数按 repack 后的块数算

BaseSynthesizer 接收 QueryBundleNodeWithScore[],按 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 线性上升。

compactCompactAndRefine)。 先尽量把小块拼进上下文窗口,再完全复用 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_onlysource_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_enginecontextcondense_plus_context 随后还要 as_retriever。传入的关键字必须同时能被这些工厂消化,错误参数可能在模式分派前就失败。

有历史时,condense_plus_context 会先 llm.complete() 改写追问,用改写结果检索,再用原始用户句子做合成。所以一次用户消息可能对应两次 LLM 调用,加上 compact/refine 自身的 K 次,延迟要从这条加法看,不能只看生成阶段的流式输出。首轮、空历史或 skip_condense=True 才省掉改写。context 模式不改写,适合短追问仍带得动原词的场景;代词很多时,不改写会检索漂移。

记忆请用:

1
2
from llama_index.core.memory import Memory
memory = Memory.from_defaults(token_limit=6000)

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
2
llama-server :8080  →  OpenAILike          →  chat / 合成 / 追问改写
llama-server :8081 → OpenAILikeEmbedding → 建索引 / 查询向量

进程内的 llama_index.llms.llama_cpp.LlamaCPP 是另一条部署,不要和 HTTP 客户端混在同一套 Settings 里。混用的典型后果是:嵌入走服务、生成走进程内,上下文窗口、模板和并发模型对不上,出了错两边日志都只看到一半。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from llama_index.llms.openai_like import OpenAILike
from llama_index.embeddings.openai_like import OpenAILikeEmbedding

llm = OpenAILike(
model="Qwen2.5-7B-Instruct",
api_base="http://127.0.0.1:8080/v1",
api_key="local-placeholder",
is_chat_model=True,
is_function_calling_model=False,
context_window=8192,
)
embed = OpenAILikeEmbedding(
model_name="bge-m3",
api_base="http://127.0.0.1:8081/v1",
api_key="local-placeholder",
)

这些类来自独立集成包,需要另行安装。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_onlycompacttree_summarize,只比较调用次数和答案是否丢掉约束条件;不要同时改 top-k。再拿一句带代词的追问,对比 contextcondense_plus_context 实际拿去检索的字符串。本地路径上,把 is_chat_model 故意关掉一次,看输出是否变成模板回显——这能帮你记住 chat completions 与 completion prompt 不是一回事。

下一章补齐 Agent、属性图,以及持久化、多租户这类工程约束。本地模型只要还把 function calling 关掉,工具循环就按 ReAct 来读;不要指望把 ChatEngine 的模式拧到已经删除的 REACT 上会自动出现 Agent。对话引擎管“带着历史怎么检索和合成”,Agent 管“要不要调用工具”,二者不要混成一个开关。

文章互动

阅读 --

留言

0 条留言

正在加载留言…