CH1_02_llamaindex_example
相比 LangChain 更像一个通用的 LLM 编排工具,LlamaIndex 更专注于 “数据索引与检索” 这个 RAG 核心场景,开箱即用。
1. 库导入
import os# os.environ['HF_ENDPOINT']='https://hf-mirror.com'from dotenv import load_dotenvfrom llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settingsfrom llama_index.embeddings.huggingface import HuggingFaceEmbeddingfrom llama_index.llms.deepseek import DeepSeek
load_dotenv()1.1 核心库解析
| 导入语句 | 作用 |
|---|---|
from dotenv import load_dotenv | 读取 .env 文件中的环境变量(如 API Key) |
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings | 三大核心组件,见下方详解 |
from llama_index.embeddings.huggingface import HuggingFaceEmbedding | 使用 HuggingFace 上的嵌入模型,将文本转为向量 |
from llama_index.llms.deepseek import DeepSeek | LlamaIndex 对 DeepSeek 大语言模型的封装,用于回答问题 |
1.2 🧩 核心函数详解
SimpleDirectoryReader — 数据加载器
- 从指定文件(或文件夹)中读取文档内容
- 支持 Markdown、PDF、Word、TXT 等多种格式
- 返回一个
Document对象列表(每个文件/页面是一个 Document)
Settings — 全局配置
- 一个全局单例配置对象,设置后整个项目自动生效
Settings.llm:指定用哪个大模型来回答问题Settings.embed_model:指定用哪个模型将文本转为向量(embedding)
HuggingFaceEmbedding — 嵌入模型
- 调用 HuggingFace 上的文本嵌入模型(这里是 BAAI 的
bge-small-zh-v1.5) - 作用:把一段文字转换成一个固定长度的向量(数字数组),用于相似度搜索
bge-small-zh-v1.5是一个轻量级的中文嵌入模型,速度快且效果不错
DeepSeek — LLM 封装
- 将 DeepSeek 的 API 封装成 LlamaIndex 能调用的 LLM 接口
model参数指定模型名,api_key从环境变量读取
VectorStoreIndex — 向量索引(核心!)
- 将 Document 列表构建成一个可检索的向量索引,内部自动完成:
- 文本分块(把长文档切成小段)
- 向量化(用
embed_model给每个块生成向量) - 存入内存向量存储(默认的内存索引)
2. 模型传入
Settings.llm = DeepSeek( model="deepseek-v4-flash", api_key=os.getenv("DEEPSEEK_API_KEY"),)Settings.embed_model = HuggingFaceEmbedding("BAAI/bge-small-zh-v1.5")指定回答问题的大模型与向量嵌入模型。
docs = SimpleDirectoryReader(input_files=["../../data/C1/markdown/easy-rl-chapter1.md"]).load_data()
index = VectorStoreIndex.from_documents(docs)
query_engine = index.as_query_engine()这里我们用 .as_query_engine() 查询引擎将 index 包装为一个问答引擎,调用的时候会自动执行检索 + 生成的全过程。
它的内部流程大致是:用户问题 → 向量检索 Top-K 相关段落 → 拼到 Prompt 中 → 发给 LLM 回答
3. 输出
print(query_engine.get_prompts())
print(query_engine.query("文中举了哪些例子?"))输出为:
{'response_synthesizer:text_qa_template': SelectorPromptTemplate(metadata={'prompt_type': <PromptType.QUESTION_ANSWER: 'text_qa'>}, template_vars=['context_str', 'query_str'], kwargs={}, output_parser=None, template_var_mappings={}, function_mappings={}, default_template=PromptTemplate(metadata={'prompt_type': <PromptType.QUESTION_ANSWER: 'text_qa'>}, template_vars=['context_str', 'query_str'], kwargs={}, output_parser=None, template_var_mappings=None, function_mappings=None, template='Context information is below.\n---------------------\n{context_str}\n---------------------\nGiven the context information and not prior knowledge, answer the query.\nQuery: {query_str}\nAnswer: '), conditionals=[(<function is_chat_model at 0x000001E63D7F40E0>, ChatPromptTemplate(metadata={'prompt_type': <PromptType.CUSTOM: 'custom'>}, template_vars=['context_str', 'query_str'], kwargs={}, output_parser=None, template_var_mappings=None, function_mappings=None, message_templates=[ChatMessage(role=<MessageRole.SYSTEM: 'system'>, additional_kwargs={}, blocks=[TextBlock(block_type='text', text="You are an expert Q&A system that is trusted around the world.\nAlways answer the query using the provided context information, and not prior knowledge.\nSome rules to follow:\n1. Never directly reference the given context in your answer.\n2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.")]), ChatMessage(role=<MessageRole.USER: 'user'>, additional_kwargs={}, blocks=[TextBlock(block_type='text', text='Context information is below.\n---------------------\n{context_str}\n---------------------\nGiven the context information and not prior knowledge, answer the query.\nQuery: {query_str}\nAnswer: ')])]))]), 'response_synthesizer:refine_template': SelectorPromptTemplate(metadata={'prompt_type': <PromptType.REFINE: 'refine'>}, template_vars=['query_str', 'existing_answer', 'context_msg'], kwargs={}, output_parser=None, template_var_mappings={}, function_mappings={}, default_template=PromptTemplate(metadata={'prompt_type': <PromptType.REFINE: 'refine'>}, template_vars=['query_str', 'existing_answer', 'context_msg'], kwargs={}, output_parser=None, template_var_mappings=None, function_mappings=None, template="The original query is as follows: {query_str}\nWe have provided an existing answer: {existing_answer}\nWe have the opportunity to refine the existing answer (only if needed) with some more context below.\n------------\n{context_msg}\n------------\nGiven the new context, refine the original answer to better answer the query. If the context isn't useful, return the original answer.\nRefined Answer: "), conditionals=[(<function is_chat_model at 0x000001E63D7F40E0>, ChatPromptTemplate(metadata={'prompt_type': <PromptType.CUSTOM: 'custom'>}, template_vars=['context_msg', 'query_str', 'existing_answer'], kwargs={}, output_parser=None, template_var_mappings=None, function_mappings=None, message_templates=[ChatMessage(role=<MessageRole.USER: 'user'>, additional_kwargs={}, blocks=[TextBlock(block_type='text', text="You are an expert Q&A system that strictly operates in two modes when refining existing answers:\n1. **Rewrite** an original answer using the new context.\n2. **Repeat** the original answer if the new context isn't useful.\nNever reference the original answer or context directly in your answer.\nWhen in doubt, just repeat the original answer.\nNew Context: {context_msg}\nQuery: {query_str}\nOriginal Answer: {existing_answer}\nNew Answer: ")])]))])}文中举了选择餐馆、做广告、挖油和玩游戏的例子。上面的一整段输出中其实就包含两个 prompt 模板:
response_synthesizer:text_qa_template:主要问答模板,你可以在开头找到它;response_synthesizer:refine_template:精炼模板,在这一大段输出中间。
我们接着来一个个看里面的参数:
text_qa_template:问答主模板,这是 LLM 首次回答时用的 prompt,在第一个长长的context_str前后。context_str:检索到的 相关文本片段,拼到这里;query_str:用户的 原始问题;Answer:留给 LLM 输出答案的位置。
Context information is below.---------------------{context_str}---------------------Given the context information and not prior knowledge, answer the query.Query: {query_str}Answer:这里的 “Given the …” 提示词是 query_engine 内置的,因为我们在创建查询引擎 query_engine = index.as_query_engine() 的时候没有传入任何参数,所以它内部自动使用了 LlamaIndex 的默认 prompt 模板。
如果想要创建自己的模板或 prompt,应该类似:
from llama_index.core import PromptTemplate
custom_qa_prompt = PromptTemplate( "请根据以下内容回答问题。\n" "内容:{context_str}\n" "问题:{query_str}\n" "答案:")
query_engine = index.as_query_engine( text_qa_template=custom_qa_prompt)is_chat_model:Chat 模型条件分支。因为 Deepseek 是 Chat 模型,所以实际用的是下面这个版本。role=SYSTEM:系统角色指令,约束回答风格;role=USER:包含上下文和用户问题的消息。
System: You are an expert Q&A system... 1. Never directly reference the given context in your answer. 2. Avoid statements like 'Based on the context, ...'
User: Context information is below. --------------------- {context_str} --------------------- Given the context information and not prior knowledge, answer the query. Query: {query_str} Answer:refine_template:精炼模板。当文档被切分成多个块,依次检索时,除了第一块用上面的主模板,后续块用这个模板精炼答案。query_str:原始问题;existing_answer:上一轮已有的答案(需要被精炼);context_msg:新的补充上下文(当前检索到的另一块文本);Refined Answer::LLM 输出优化后答案的位置。
# Chat 分支版本,带 Sytem/User 角色System: ...1. **Rewrite** an original answer using the new context. 2. **Repeat** the original answer if the new context isn't useful. Never reference the original answer or context directly...
User: New Context: {context_msg} Query: {query_str} Original Answer: {existing_answer} New Answer:综上,完整的流程如下图:
用户问:"文中举了哪些例子?" │ ▼ 向量检索 Top-K 个相关块 │ ┌─────┴─────┐ │ 第 1 块 │ ──→ text_qa_template ──→ LLM → 初步答案 A1 └─────┬─────┘ │ ┌─────┴─────┐ │ 第 2 块 │ ──→ refine_template (A1 + 新块) → 精炼答案 A2 └─────┬─────┘ │ ┌─────┴─────┐ │ 第 3 块 │ ──→ refine_template (A2 + 新块) → 最终答案 A3 └───────────┘其他元数据补充:
prompt_type:模板类型标识,这里是QUESTION_ANSWER(问答)和REFINE(精炼);template_vars:模板中用到的变量名列表conditionals:条件分支 —— 根据模型类型(是否是 Chat 模型)选择不同的模板格式default_template:文本补全模型的默认模板(text-davinci-003风格)
3.1 什么是 Chat 模型?
在 LlamaIndex 的语境里,is_chat_model 的判断对应的是 LLM 的两种 API 调用方式:
1️⃣ Chat 模型(如 DeepSeek、GPT-4o、GPT-3.5-turbo、Claude 等)
- API 格式:接受
System+User+Assistant角色的消息列表(Messages) - 特点:支持多轮对话、角色分离、指令遵循能力强
- LlamaIndex 对 Chat 模型使用
ChatPromptTemplate,如我们看到的带role=SYSTEM和role=USER的模板
2️⃣ 文本补全模型(Text Completion / 原始 LLM)
- API 格式:接受一段连续文本字符串,模型接着往后生成
- 代表:OpenAI 早期的
text-davinci-003、text-curie-001、gpt-3.5-turbo-instruct等 - 特点:没有 System/User 角色之分,全靠 Prompt 本身的措辞来引导
LlamaIndex 对这类模型使用 default_template,就是一个纯文本模板:
Context information is below.---------------------{context_str}---------------------Given the context information and not prior knowledge, answer the query.Query: {query_str}Answer:3.1.3 区别对比
| 维度 | Chat 模型 | 文本补全模型 |
|---|---|---|
| 输入格式 | 消息列表([System, User, Assistant]) | 纯文本字符串 |
| 角色区分 | ✅ 有 System / User 角色 | ❌ 无角色,全部混合在文本中 |
| 指令位置 | System 消息放系统指令 | 指令写在 Prompt 开头/结尾 |
| 代表模型 | DeepSeek, GPT-4o, Claude, Gemini | text-davinci-003, gpt-3.5-turbo-instruct |
| LlamaIndex 模板 | ChatPromptTemplate | PromptTemplate(即 default_template) |
3.1.4 实际效果差异
对于同一个意图,模型实际收到的内容分别是:
文本补全模型(纯文本)→
Context information is below.---------------------文中提到选择餐馆...---------------------...Answer:Chat 模型(消息列表)→
System: You are an expert Q&A system... 1. Never directly reference the given context...
User: Context information is below. --------------------- 文中提到选择餐馆... --------------------- ... Answer:Chat 模型多了 System message 这个专门放指令的位置,指令不会被用户问题”淹没”,所以 LlamaIndex 在 Chat 模型的模板里加了更多细粒度的约束(如”不要提到 ‘根据上下文‘“等),让回答更自然。
现在几乎所有主流 API 都是 Chat 模型了,OpenAI 也在 2024 年彻底弃用了 text-davinci-003 等补全模型。所以我们大概率只会用到 Chat 模型分支,default_template 更多是历史兼容用途。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!