安装试验

  • llama-index
  • llama-index-llms-ollama
  • llama-index-embeddings-ollama
  • llama-index-readers-file 可以读取多种文档,默认只能存文本 结合 SimpleDirectoryReader 一起使用解析文档
  • python-docx 解析docx文档
  • pandas openpyxl xlrd 解析xlsx文档
  • chromadb llama-index-vector-stores-chroma
  • llama-index-retrievers-bm25 jieba (pickle为内置模块无需安装) 1万以下文档
documents = SimpleDirectoryReader(
    input_dir="data",
    extensions=[".docx", ".xlsx"],  # 精准筛选格式,避免读取无关文件
    recursive=True  # 可选:是否递归读取子目录内的文件(True=递归,False=仅根目录)
).load_data()

文件解析

pip install llama-index-readers-file 
pip install python-docx pandas  openpyxl  xlrd

核心安装

pip install llama-index
pip install llama-index-embeddings-ollama llama-index-llms-ollama

外置保存

pip install llama-index-retrievers-bm25 jieba pickle
pip install chromadb llama-index-vector-stores-chroma 

读取文件创建关键词BM25 可以增量但是删除修改要重建

from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
from llama_index.retrievers import bm25
import jieba

def chinese_tokenizer(text: str) -> List[str]:
    return list(jieba.cut(text))

# 1. 读文档 切分
documents = SimpleDirectoryReader("data").load_data()
splitter = SentenceSplitter(chunk_size=512, chunk_overlap=30)
new_nodes = splitter.get_nodes_from_documents(documents)


retriever = bm25.BM25Retriever(nodes=new_nodes, similarity_top_k=10)
retriever.tokenizer = chinese_tokenizer

#此处可以省略
# 3. 默认查询
results = retriever.retrieve("author growing up")
print(f"召回 {len(results)} 条")

#新增多个文件切分

input_files = ["data/file1.docx", "data/file2.txt", "data/file3.pdf"]
docs = SimpleDirectoryReader(input_files=input_files).load_data()

# 2. 直接切分(无需区分格式)
splitter = SentenceSplitter(chunk_size=512, chunk_overlap=30)
nodes = splitter.get_nodes_from_documents(docs)  # 一步到位

#重新加入文件
retriever = bm25.BM25Retriever(nodes=new_nodes, similarity_top_k=10)
retriever.tokenizer = chinese_tokenizer

#此处可以省略
# 3. 默认查询
results = retriever.retrieve("author growing up??")
print(f"召回 {len(results)} 条")


#上面第二步开始即可调用
# 1. pickle存储
import pickle

with open("./idx/bm25.pkl", "rb") as f:
    bm25_retriever = pickle.load(f)

# 可热调参数
bm25_retriever.similarity_top_k = 15
##-----------

from llama_index.core import SimpleDirectoryReader

# 1. 读新增文件
new_docs = SimpleDirectoryReader("new_data").load_data()

# 2. 增量加入(官方支持 add_nodes)

# 4. ✅ 必须先切成 Node(官方示例缺失这一步)
splitter = SentenceSplitter(chunk_size=512, chunk_overlap=30)
new_nodes = splitter.get_nodes_from_documents(new_docs)

# 5. ✅ 只能重建


# 3.  都使用PICKLE覆盖
Path("./idx").mkdir(exist_ok=True)
with open("./idx/bm25.pkl", "wb") as f:
    pickle.dump({"nodes": nodes, "top_k": 10, "tokenizer": chinese_tokenizer}, f)

# 查询(官方示例 ⑤ —— retrieve)
results = bm25_retriever.retrieve("作者童年做了什么")
for node in results:
    print(node.text[:200], node.score)


#融合
from llama_index.core.retrievers import QueryFusionRetriever
from llama_index.core import VectorStoreIndex

# 1. 向量检索器
vector_index = VectorStoreIndex.from_documents(documents)
vector_retriever = vector_index.as_retriever(similarity_top_k=10)

# 2. BM25 检索器(已加载)pickle.load加载
bm25_retriever = pickle.load(open("./idx/bm25.pkl", "rb"))





# 3. 融合
fusion_retriever = QueryFusionRetriever(
    [vector_retriever, bm25_retriever],
    similarity_top_k=10,
)

# 4. 后处理链(同前)
query_engine = RetrieverQueryEngine(
    retriever=fusion_retriever,
    node_postprocessors=[...],
)





中文优化 jieba 要在from_documents中加入tokenizer=chinese_tokenizer

import jieba

def chinese_tokenizer(text: str) -> List[str]:
    return list(jieba.cut(text))

# 1. 读取全部文档(含增量目录)
all_docs = (SimpleDirectoryReader("data").load_data() +
            SimpleDirectoryReader("new_data").load_data())
nodes = SentenceSplitter(chunk_size=512, chunk_overlap=30).get_nodes_from_documents(all_docs)

bm25.tokenizer = chinese_tokenizer
retriever = bm25.BM25Retriever(nodes=nodes, similarity_top_k=10)

生成一个简单能用的:

# -*- coding: utf-8 -*-
# 0.6.0 实测接口:只有 retrieve,没有 add/add_nodes —— 只能“一次性重建”
import pickle, jieba
from pathlib import Path
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
from llama_index.retrievers import bm25

def chinese_tokenizer(text: str) -> list[str]:
    return list(jieba.cut(text))

# 1. 读取全部文档(含增量目录)
all_docs = (SimpleDirectoryReader("data").load_data() +
            SimpleDirectoryReader("new_data").load_data())

# 2. 切节点
nodes = SentenceSplitter(chunk_size=512, chunk_overlap=30).get_nodes_from_documents(all_docs)

# 3. 建 retriever

retriever = bm25.BM25Retriever(nodes=nodes, similarity_top_k=10)
retriever.tokenizer = chinese_tokenizer
#retriever.stemmer = None
# 4. 落盘
Path("./idx").mkdir(exist_ok=True)
with open("./idx/bm25.pkl", "wb") as f:
    pickle.dump({"nodes": nodes, "top_k": 10, "tokenizer": chinese_tokenizer}, f)

# 5. 查询
for n in retriever.retrieve("what is look?"):
    print(n.score, n.text[:200])
  1. 按权限标签/用户把原始文档切成 N 个物理子文件夹(或数据库分区)→
  2. 每个子集分别建 BM25 pickle + 向量索引
  3. 查询时先根据当前用户权限选对应的 pickle & 向量库 →
  4. 用同一套混合逻辑(RRF/加权)召回 →
  5. 最终返回结果。

这样:

  • BM25 召回范围天然受限于子集,不会越权
  • 向量侧仍保留 metadata,可做更细粒度二次过滤展示字段
  • 子库之间完全隔离,加用户/改权限只需新增/重建对应子库,不影响别人。

新的 添加节点 测试通过BM25版本0.6.0

from typing import List
import jieba
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
from llama_index.retrievers.bm25 import BM25Retriever

# ===================== 核心适配:重写BM25Retriever类以支持中文分词 =====================
class ChineseBM25Retriever(BM25Retriever):
    """适配0.6.0版本的中文BM25检索器(重写分词方法)"""
    def _tokenize(self, text: str) -> List[str]:
        """重写内部分词方法,替换为jieba中文分词"""
        # 过滤空字符串,避免影响BM25计算
        return [word for word in jieba.cut(text) if word.strip()]

# ===================== 1. 初始加载文档 + 切分 + 初始化检索器 =====================
# 加载初始目录下的所有文档
documents = SimpleDirectoryReader("data").load_data()
# 统一初始化切分器(仅初始化一次,保证切分规则一致)
splitter = SentenceSplitter(chunk_size=512, chunk_overlap=30)
# 切分初始文档为节点
original_nodes = splitter.get_nodes_from_documents(documents)

# 【关键修改】使用自定义的中文BM25检索器(0.6.0版本无tokenizer参数)
retriever = ChineseBM25Retriever(
    nodes=original_nodes, 
    similarity_top_k=10  # 仅保留支持的参数
)

# 初始查询
first_results = retriever.retrieve("what is look")
print(f"初始查询 - 召回 {len(first_results)} 条")

# ===================== 2. 新增多个文件 + 切分 + 增量追加 =====================
# 加载新增的混合格式文件
input_files = ["new_data/fa.txt", "new_data/fa2.txt"]
new_docs = SimpleDirectoryReader(input_files=input_files).load_data()
# 用同一个切分器切分新增文件
new_nodes = splitter.get_nodes_from_documents(new_docs)

# 合并所有节点后重新初始化检索器(0.6.0版本无nodes属性,只能重新初始化)
all_nodes = original_nodes + new_nodes
retriever = ChineseBM25Retriever(
    nodes=all_nodes,
    similarity_top_k=10
)

# ===================== 3. 新增文件后重新查询 =====================
second_results = retriever.retrieve("what is look?")
print(f"新增文件后查询 - 召回 {len(second_results)} 条")

# 验证节点数
print(f"\n初始节点数:{len(original_nodes)}")
print(f"新增节点数:{len(new_nodes)}")
print(f"当前检索器总节点数:{len(all_nodes)}")

文档添加多个metadata数据

from pathlib import Path
from typing import Dict

def make_meta_func(department: str, upload_user: str):
    """工厂:返回一个符合签名的 metadata 函数"""
    def rich_meta(fp: Path) -> Dict[str, str]:
        stat = fp.stat()
        return {
            "file_name": fp.name,
            "department": department,   # 外部绑进来的
            "upload_user": upload_user, # 外部绑进来的
            "create_time": datetime.fromtimestamp(stat.st_ctime).isoformat(),
            "confidential": "internal",
        }
    return rich_meta

# 使用
documents = SimpleDirectoryReader(
    "data",
    file_metadata=make_meta_func(department="ABC", upload_user="sam")
).load_data()

LLAMAINDEX面试

  1. 基础 RAG 流程(VectorStoreIndex → Retriever → Synthesizer)
  2. 过滤器体系(metadata、相似度、关键词、时间、LLM 重排)
  3. 多路召回与融合(QueryFusionRetriever)
  4. 混合检索(向量 + 关键词)
  5. 节点后处理链(Postprocessor 顺序、扩上下文、引用来源)
  6. 模板与占位符、输出解析、结构化引用
  7. 模型角色划分(embedding vs LLM)与 Ollama 本地部署
  8. 元数据治理(文件名、时间、部门、密级)

→ 这 8 大块基本覆盖了 90 % 的 LlamaIndex 面试题:

  • “召回阶段有哪些Retriever?”
  • “过滤器执行顺序?”
  • “LLM 重排原理?”
  • “怎么给答案加引用?”
  • “向量与关键词如何混合?”
  • “metadata 缺失怎么办?”

所以确实够应付一轮中级/高级面试;真要被问到更深,一般就两条延伸:

  1. 源码级IndexStoreVectorStore 自定义实现,NodeParser 嵌套,异步 aretrieve 流程;
  2. 生产级:分布式索引、增量更新、多租户权限、量化部署、重排模型微调。

关键词检索

from llama_index.core import SimpleKeywordTableIndex
#创建关键词引索
keyword_index = SimpleKeywordTableIndex.from_documents(documents)

#检索器导入 1向量 2关键词 3混合
from llama_index.core.retrievers import (
    VectorIndexRetriever,
    KeywordTableSimpleRetriever,
    QueryFusionRetriever,
)

# 1. 向量召回 10 条
vector_ret = VectorIndexRetriever(
    index=vector_index,
    similarity_top_k=10,
    filters=filtersABC,  # 元数据过滤仍可用
)
# 2. 关键词召回 10 条
keyword_ret = KeywordTableSimpleRetriever(
    index=keyword_index,
    keywords=["author", "childhood"],  # 也可放空,让框架自动抽关键词,这里添加可以让后续抽中更多关键词
)

# 3. 融合:去重 + 向量相似度重排(默认 fusion_mode="reciprocal_rerank")
fusion_retriever = QueryFusionRetriever(
    [vector_ret, keyword_ret],  # 并行跑
    similarity_top_k=10,        # 融合后最终保留 10 条
)

#后面简写
query_engine = RetrieverQueryEngine(
    retriever=fusion_retriever,  # 只用换这一行
    response_synthesizer=get_response_synthesizer(),
    node_postprocessors=[
        SimilarityPostprocessor(similarity_cutoff=0.7),
        KeywordNodePostprocessor(...),  # 仍可再卡关键词
        FixedRecencyPostprocessor(top_k=7, date_key="date"),
        LLMRerank(top_n=3, choice_batch_size=3),
        PrevNextNodePostprocessor(num_prev=1, num_next=1),
    ],
)

关键词后处理KeywordNodePostprocessor后处理阶段字面关键词硬过滤器——
向量或关键词召回的节点已经拿到手以后,再用字符串包含/排除的方式筛一遍,不走路径、不走模型,纯文本匹配。

不管它是向量检索召回的、关键词检索召回的、还是两者融合后的,KeywordNodePostprocessor 只认节点文本本身

  • 文本里同时包含required_keywords 所有词 → 放行
  • 文本里一旦出现exclude_keywords 任意词 → 整段扔掉

不关心节点最初来自哪个索引、哪个算法,只要最终进了 node_postprocessors 列表,就会被逐个字面扫描一遍。

from llama_index.core.postprocessor import KeywordNodePostprocessor

key_filter = KeywordNodePostprocessor(
    required_keywords=["author", "childhood"],  # 同时出现才保留
    exclude_keywords=["fiction", "novel"]       # 出现一个就扔掉
)

LLAMAindex 响应器 现实引用原文提示词加入metadata占位符

响应器(response_synthesizer)负责把最终节点列表 + 用户问题拼成提示词,调用大模型生成自然语言答案。它不参与检索、不过滤、不重排,只在链路最末端做“阅读-理解-回答”。

from llama_index.core import VectorStoreIndex, get_response_synthesizer


response_synthesizer = get_response_synthesizer()
query_engine = RetrieverQueryEngine(
    retriever=retriever,
    response_synthesizer=response_synthesizer, #响应器默认
    node_postprocessors=[...]   # 各种过滤器/重排/扩上下文
)

node_postprocessors处理完的节点response_synthesizerLLM 返回 Response

A 把节点文本切成 text_qa_template 能容纳的 token 窗口
B 响应器的两种工作状态

1.全部打包(compact 默认)

2.分块迭代(refine 先拿第一段生成初版答案,再依次把后续段落 refine 进去)

C 拼好 prompt → 调 Settings.llm.complete() → 解析返回内容 → 包成 Response 对象

更换策略 从默认的compact 变为refine

response_synthesizer = get_response_synthesizer(
    response_mode="refine",      # 或 "compact", "tree_summarize", "simple_summarize", "no_text"
    llm=Ollama(model="qwen3-14b")  # 用更大的模型专门负责生成答案
)

还可以自定义prompt模板 ,精华部分!默认compact使用text_qa_template参数;refine模式使用refine_template模板设置。

from llama_index.core import PromptTemplate

qa_tmpl = PromptTemplate(
    "你是小助手,请依据下列资料回答,若信息不足请说“不知道”。\n"
    "资料:{context_str}\n问题:{query_str}\n答案:"
)

response_synthesizer = get_response_synthesizer(
    text_qa_template=qa_tmpl,
    refine_template=...          # 如果选 refine 模式再配 refine 模板
)

这两个占位符

  • {context_str} – 框架会把“经过所有 postprocessor 处理后的节点文本”自动拼成一段塞进来
  • {query_str} – 用户当前的问题

下面的模板

资料:{context_str}
问题:{query_str}
答案:

添加文章来源

在 LlamaIndex 里想“给答案加引用”有两种常用做法,都只需要改响应合成器的模板,让它把节点自带的 metadata 打印出来即可;


因此metadata数据的完善性尤为重要:
文件名称,创建时间,修改时间,上传时间,上传人,上传部门,文件类型,机密类型,hash值,上传的IP

from llama_index.core import PromptTemplate

# {file_name} 就是 metadata 里的字段,可随意加
cite_tmpl = PromptTemplate(
    "资料:{context_str}\n\n"
    "问题:{query_str}\n\n"
    "答案:\n"
    "----------\n"
    "出处:{file_name}"  # 这里会被替换成真实文件名
)

response_synthesizer = get_response_synthesizer(
    text_qa_template=cite_tmpl,
    response_mode="compact"
)

现实来源总结

  • 有字段 → 自动替换
  • 无字段 → 原样保留占位符
  • 不会报错,也不会自动补空值