项目环境搭建
- 如何将requirements.txt中的依赖转移到 uv环境中
uv add -r requirements.txt- 如果太慢则加 url
uv add -r requirements.txt --index-url https://pypi.tuna.tsinghua.edu.cn/simple
- 如果太慢则加 url
- ollama搭建
- 报错
- ollama修改模型下载目录之后报错
- error: pull model manifest: 503: no healthy upstream
- 重启服务即可
- sudo systemctl stop ollama
- sudo systemctl start ollama
- 修改模型目录
- sudo ln -s /home/chester/ollama/models /usr/share/ollama/.ollama/
- sudo chown ollama /usr/share/ollama/.ollama/models
- sudo chgrp ollama /usr/share/ollama/.ollama/models
- 后两条是保证目录的权限问题,不然ollma运行会报错
- 资料来源 Does anyone know how to change where your models are saved on linux? : r/ollama
- Ollama(model=“qwen3:0.6b”)
- 这段代码前提是 系统中 后台启动了ollama服务(linux安装之后会自动加入系统服务,无需手动启动该服务),但并不需要 命令行执行 ollama run model。应该是后台会自动吊起对应的模型进行计算并传回python。
- 本地运行ollama真的太慢了。
- 报错
- embedding推理服务方法
- HuggingFaceBgeEmbeddings
- 在pycharm中运行的时候,要给jupyter server 添加参数,配上http_proxy的代理。在pycharm软件层面设置http_proxy是没用的。
- huggingface会下载模型建立本地服务进行计算。
- HuggingFaceInferenceAPIEmbeddings
- 是通过api进行embedding计算,而不是本地进行计算。
- 问题:httpcore 一直报错没有安装
- 最后发现是httpcore没有安装好,对应的python包只有元数据没有真实代码,将对应目录删除后重刷uv环境即可。
- HuggingFaceBgeEmbeddings
P01 RAG-Projects
以下为小的RAG子项目
Project 01 chatbot
问题:load_dotenv的机制
- load_dotenv 定义如下
- 默认加载 .env文件,如果当前目录不存在,就不断提升目录层次来查找。
- interpolate 用于是否解析字符串中的变量,比如
PATH=${PATH}:/usr/share/bin,就会把PATH变量进行解析替换。
- 其中
dotenv.main.resolve_variables对于interpolate 功能的实现很有意思,使用了正则方法来匹配并分别进行替换。
def load_dotenv(
dotenv_path: Optional[StrPath] = None,
stream: Optional[IO[str]] = None,
verbose: bool = False,
override: bool = False,
interpolate: bool = True,
encoding: Optional[str] = "utf-8",
) -> bool:
"""Parse a .env file and then load all the variables found as environment variables.
Parameters:
dotenv_path: Absolute or relative path to .env file.
stream: Text stream (such as `io.StringIO`) with .env content, used if
`dotenv_path` is `None`.
verbose: Whether to output a warning the .env file is missing.
override: Whether to override the system environment variables with the variables
from the `.env` file.
encoding: Encoding to be used to read the file.
Returns:
Bool: True if at least one environment variable is set else False
If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the
.env file with it's default parameters. If you need to change the default parameters
of `find_dotenv()`, you can explicitly call `find_dotenv()` and pass the result
to this function as `dotenv_path`.
If the environment variable `PYTHON_DOTENV_DISABLED` is set to a truthy value,
.env loading is disabled.
"""
Project 02 APIs
ChatMessagePromptTemplate和ChatPromptTemplate是两个不同的类,主要区别如下:- ChatMessagePromptTemplate
- 用于表示单个聊天消息的模板
- 对应单条消息,包含消息的角色(如系统、用户、助手)和内容模板
- 是构成聊天模板的基本单元
- ChatPromptTemplate
- 用于表示整个聊天对话的模板
- 可以包含多个
ChatMessagePromptTemplate实例 - 用于构建完整的对话历史模板,支持多轮对话场景
- 简而言之,
ChatMessagePromptTemplate是单条消息模板,而ChatPromptTemplate是整个对话模板,后者可以包含多个前者来构建完整的对话上下文。
- ChatMessagePromptTemplate
ChatPromptTemplate- from_messages 从若干message中构造模板
- from_template 从单个 message 的 template 来构造 HumanMessage,并组装为一个Chat。
FewShotChatMessagePromptTemplate- langchain_core.prompts中的各种prompt。
- ChatOpenAI的使用方法
- StrOutputParser 如何解析chain之前部分的内容
- langchain_core.outputs.chat_generation.ChatGeneration.set_text 中指明了从message中的content属性来获取内容。
Project 04 Retriever and Chain
- create_stuff_documents_chain
- RunnablePassthrough 即 identity function,恒等函数,输入什么就输出什么。
- RunnablePassthrough.assign : Merge the Dict input with the output produced by the mapping argument.
- chain的调用过程
- 输入:输入数据为dict,有input这个key。
retrieval_docs:先对 数据进行检索,即从x中获取 input这一项,然后送入检索器。检索器本质上是调用了__ror__方法,进一步调用了VectorStoreRetriever的_get_relevant_documents方法,进一步调用了self.vectorstore.similarity_search方法。检索完之后,将相似文档组装为一个item放入到 context这个key下。 数据变为{input:XXX, context:[docs]}。format_docs:检索出相似文档之后,送入format_inputs过程,即将数据中的context项格式为字符串,并用 双换行符进行分隔连接。数据变为{input:XXX, context:doc_str}prompt: 送入prompt,prompt的invoke方法中本质就是对 template字符串和 上一步计算出的数据进行格式化。llm:模型给出响应_output_parser本质就是对 输出的response消息进行格式化,本质就是提取消息中的key为"text"对应的属性。
相关关键代码如下
response=retriever.invoke(input="""image processing with an introduction to techniques for image pattern classification""")
#langchain_classic/chains/retrieval.py:64
if not isinstance(retriever, BaseRetriever):
retrieval_docs: Runnable[dict, RetrieverOutput] = retriever
else:
retrieval_docs = (lambda x: x["input"]) | retriever
return (
RunnablePassthrough.assign(
context=retrieval_docs.with_config(run_name="retrieve_documents"),
).assign(answer=combine_docs_chain)
).with_config(run_name="retrieval_chain")
def format_docs(inputs: dict) -> str:
return document_separator.join(
format_document(doc, _document_prompt)
for doc in inputs[document_variable_name]
)
return (
RunnablePassthrough.assign(**{document_variable_name: format_docs}).with_config(
run_name="format_inputs",
)
| prompt
| llm
| _output_parser
).with_config(run_name="stuff_documents_chain")
Project 05 Advanced RAG Q&A Project
- create_retriever_tool
- 本质就是 将检索、格式化、拼接工作组装为一个整体功能然后,实例化为一个tool。
- Scratchpad 含义
- 直译为草稿纸
- 在 LangChain 或类似框架中,
scratchpad通常指代理(Agent)的临时工作空间,用于:- 存储中间思考过程
- 记录当前任务状态
- 保存临时计算结果
- 进行推理和决策的缓冲区
- 这是一个通用的计算机科学概念,表示任何用于临时存储和处理的内存区域。
- 最终tools是在
ollama._types.ChatRequest进行解析,具体见下面代码。 - 这里的llm走的是chat类型的llm,所以底层是走
/api/chat接口,该接口的参数列表中含有tools。相当于该接口已经把tools的描述如何格式化都已经在内部解决掉,不需要调用方将tools的描述格式化到 prompt里面。 - AgentExecutor里最重视依赖
iter方法来不断迭代,底层是依赖_iter_next_step来进行 thought、action步骤的。 - 即chat_model的接口都是支持在请求中加入tools参数的。
- prompt hub
- 函数调用 - OpenAI API — Function calling - OpenAI API
相关关键代码如下
#Runnable的方法
def bind(self, **kwargs: Any) -> Runnable[Input, Output]:
"""Bind arguments to a `Runnable`, returning a new `Runnable`.
Useful when a `Runnable` in a chain requires an argument that is not in the output of the previous `Runnable` or included in the user input.
"""
#langchain_ollama.chat_models.ChatOllama._create_chat_stream
# ollama/_client.py:351
# ollama._client.Client.chat
return self._request(
ChatResponse,
'POST',
'/api/chat',
json=ChatRequest(
model=model,
messages=list(_copy_messages(messages)),
tools=list(_copy_tools(tools)),
stream=stream,
think=think,
format=format,
options=options,
keep_alive=keep_alive,
).model_dump(exclude_none=True),
stream=stream,
)
#langchain_classic.agents.agent.AgentExecutor._iter_next_step
def _iter_next_step(
self,
name_to_tool_map: dict[str, BaseTool],
color_mapping: dict[str, str],
inputs: dict[str, str],
intermediate_steps: list[tuple[AgentAction, str]],
run_manager: CallbackManagerForChainRun | None = None,
) -> Iterator[AgentFinish | AgentAction | AgentStep]:
"""Take a single step in the thought-action-observation loop.
Override this to take control of how the agent makes and acts on choices.
"""
try:
intermediate_steps = self._prepare_intermediate_steps(intermediate_steps)
# Call the LLM to see what to do.
output = self._action_agent.plan(
intermediate_steps,
callbacks=run_manager.get_child() if run_manager else None,
**inputs,
)
except OutputParserException as e:
......
# If the tool chosen is the finishing tool, then we end and return.
if isinstance(output, AgentFinish):
yield output
return
actions: list[AgentAction]
actions = [output] if isinstance(output, AgentAction) else output
for agent_action in actions:
yield agent_action
for agent_action in actions:
yield self._perform_agent_action(
name_to_tool_map,
color_mapping,
agent_action,
run_manager,
)
...
Project 06 Groq inference
- 本地服务和远程服务的代理设置,如下面代码
- HTTP_PROXY 和 HTTPS_PROXY用于设置通过http服务的代理
- NO_PROXY 用于本地服务绕过上面设置的代理,如本地的Ollama服务。
vectorstore.as_retriever可以直接转换为检索器- 方法参数
search_kwargs中也可以设置返回的文档数目k、MMR多样性算法、元数据过滤器函数等。
- 方法参数
import os
# 设置http的代理和不代理的地址
os.environ['HTTP_PROXY'] = "http://127.0.0.1:7890"
os.environ['HTTPS_PROXY'] = "http://127.0.0.1:7890"
os.environ['NO_PROXY'] = "http://127.0.0.1:11434" #ollama的本地服务地址
Project 07 Gen AI
- HuggingFaceEndpoint 接口问题
- bug https://github.com/langchain-ai/langchain/issues/31434#issuecomment-2936308959
- 因为官方服务不再提供 text-generation的模型服务。
- 可以使用 ChatHuggingFace来绕过这个问题。
Project 08 Powerful Doc Q&A Chatbot
- OllamaEmbeddings 在本地的笔记本上对于4篇论文的计算非常慢。
- 使用了HuggingFaceEmbeddings加一个小emb模型计算时间变少了很多。
Project 09 Advance Q&A Chatbot
- langchain-ObjectBox 版本太老,没法安装。该数据库 ObjectBox 维护似乎不是很多。
Project 11 ImageEnhancer
- model_scope 兼容接口
- API推理介绍 · 文档中心
- 兼容openai接口
- streamlit 应用 debug
- 选择module streamlit
- 参数填写 run app.py
- working directory 填写app.py的目录
- 使用modelscope服务来代替openai的图像生成接口。
- 文生图模型 支持API调用的模型列表,可以通过AIGC模型页面进行搜索。
- 上述页面中列出的很多模型都微调的很差,使用后生成结果很多并不能遵从指令。
- 调整了 gpu_memory_utilization 从0.9 到0.95
- 报错
ValueError: To serve at least one request with the models's max seq len (40960), (5.62 GiB KV cache is needed, which is larger than the available KV cache memory (4.81 GiB).
- 差一点内存,所以增加了gpu的利用率。
- 从这点看,大模型一次推理非常消耗显存。
- 一条数据的最大显存消耗
显存消耗=2(key+value) * seq_len(40960) * model_layer * hidden_size * head_dim * 2(byte FP16)
- 报错
P02 demo_simple_rag_py
该项目展示RAG原理的极简代码。
- 参考资料
- 关联博文 Code a simple RAG from scratch
- 项目代码 demo.py · ngxson/demo_simple_rag_py at main
- 很好的展示RAG原理性的小demo,不需要安装向量数据库。
- 模型ID bartowski/Llama-3.2-1B-Instruct-GGUF 这个模型中的instruct的意思
- instruct意思就是指模型是经过 在base model上经过指令微调(或者叫监督微调)过程之后产生的模型,可以直接用于聊天、助手类应用
- Ollama问题
- 模型下载问题 ollama 拉取hf.co 网站的模型失败
Ollama pull 进度已经100%, 但为什么会报如下错误Error: max retries exceeded: Get "https://huggingface.co/v2/bartowski/Llama-3.2-1B-Instruct-GGUF/blobs/sha256:948af2743fc78a328dcb3b0f5a31b3d75f415840fdb699e8b1235978392ecf85?__sign=eyJhbGciOiJFZERTQSJ9.eyJyZWFkIjp0cnVlLCJwZXJtaXNzaW9ucyI6eyJyZXBvLmNvbnRlbnQucmVhZCI6dHJ1ZX0sImlhdCI6MTc2Mjc1MzA3Niwic3ViIjoiL2JhcnRvd3NraS9MbGFtYS0zLjItMUItSW5zdHJ1Y3QtR0dVRiIsImV4cCI6MTc2Mjc1MzY3NiwiaXNzIjoiaHR0cHM6Ly9odWdnaW5nZmFjZS5jbyJ9.wVhqKCuc0vtHoS8DUStd86RoS1dhE0e-IqqMIpLZ_mEbfz6ahWeaoQiuTpWuaJCels7Q7uIIwEky9DhHsynYCg": dial tcp 54.89.135.129:443: connect: connection refused
- 检查网络是可以连接 hf.co网站的。
- 找不到原因。
- ollama 也可以在本地对模型进行量化。 quantizing-a-model - Ollama
- ollama对仓库模型的选择
- ollama run model_repo_id,如果repo中有多版本模型,那么ollama会选择Q4版本的。更好的方法是显式指定具体版本
ollama run hf.co/{username}/{repository}:{quantization}- 比如
ollama run hf.co/bartowski/Llama-3.2-3B-Instruct-GGUF:Llama-3.2-3B-Instruct-IQ3_M.gguf
- 比如
- 参考资料 Use Ollama with any GGUF Model on Hugging Face Hub
- ollama run model_repo_id,如果repo中有多版本模型,那么ollama会选择Q4版本的。更好的方法是显式指定具体版本
- 模型下载问题 ollama 拉取hf.co 网站的模型失败
- 模型文件问题
- GGUF 文件包含了tokenizer的数据。
- 模型的Ollama构建方法:手动下载gguf模型文件,然后本地进行ollama手动构建。
- 手动下载 https://huggingface.co/bartowski/Llama-3.2-1B-Instruct-GGUF/tree/main 中的 Llama-3.2-1B-Instruct-Q4_K_M.gguf文件。
- 然后根据文档Importing a Model - Ollama来导入到本地ollama的缓存目录。
- 创建一个临时空目录
- 创建 ModelFile文件,内容为
FROM ./Llama-3.2-1B-Instruct-Q4_K_M.gguf - 执行命令
ollama create bartowski/Llama-3.2-1B-Instruct-GGUF - 模型就可以被构建缓并复制到 ollama的缓存目录中。
- 并同样构建 bge-base-en-v1.5-q4_k_m.gguf 模型
ollama.embed(model="CompendiumLabs/bge-base-en-v1.5-gguf")- 这里的模型名按照/来分割,即说明是本地文件的模型。
- 如果是正常的:分割的模型名,则ollama会自动从缓存目录中的 library子目录中查找。
- 在ChatModel的stream模式下,返回是一个词一个词的进行返回的。
- 在该demo示例中,检索到的文档是全被塞到一个system message下,然后后续的对话但就是根据此来进行交互。
P03 RAG-Optimization-Practices
项目原理和结果
- 相关资料
- 阿里云服务器
- uv 下载经常超时的解决方案,非常好用。
export UV_HTTP_TIMEOUT=240export UV_CONCURRENT_DOWNLOADS=2
- 如何加快安装uv环境、缓存
- 在一台便宜的机器上使用uv来构建一个dummy项目,uv的缓存就会留下。
- rsync uv缓存目录到 目标服务器的uv缓存目录下。同一个vpc下两个服务器的rsync速度非常快,300M/s。并且rsync会保持文件的软链接。
- 这样整体花钱少,并且构建速度快。
- uv 下载经常超时的解决方案,非常好用。
- Qwen-reranker和bert一样是双向注意力计算方式。
- 对于一个batch内的数据,有很多条数据有共同的前缀,那么vLLM中有针对这种情况的优化推理策略
- 阿里云ossfs的代码存档。
- 安装ossfs
wget https://github.com/aliyun/ossfs/releases/download/v1.91.8/ossfs_1.91.8_ubuntu24.04_amd64.debsudo apt install ./ossfs_1.91.8_ubuntu24.04_amd64.deb
- 安装ossfs
- 环境配置
- A10 GPU(显存24G)
- Reranker distillation整体步骤
- 说明
- 模型本质就是cross-encoder,即将QA进行拼接,输入LLM进而得到相似度。
- generate_logits 生成59万条数据(训练集+测试集),运行时长(250min)。
- 即针对 query 和doc 生成一个logprob分数,即
(query, passage, score)。 - logprob分数利用了 prompt模板 + LLM的chat接口 进行打分。 prompt模板如下。
Judge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be "yes" or "no".<Instruct>: {instruction}\n<Query>: {query}\n<Document>: {doc}- 其中 instruct变量为
Given a web search query, retrieve relevant passages that answer the query
- prompt 限制了输出token的范围为 两个token (yes or no) 。
- 推理使用 vLLM的sdk,加载本地模型文件。
- 即针对 query 和doc 生成一个logprob分数,即
- 三元组蒸馏数据(训练集+测试集)
- 生成三元组蒸馏数据
(query, positive passage, negative passage, logits_diff) - 正样本为logits 最高top_k里抽样。
- 负样本按照logits排序 每个正样本位置后面的样本。
- 即两两组合,组合数量为
n*(n-1)/2 - 并非使用该项目数据集真正的原始 ground-truth label。
- 生成三元组蒸馏数据
- 训练
- 利用 embedding小模型 + 三元组蒸馏数据 进行训练,将领域知识蒸馏进小模型。
- 评估
- 对微调前的模型和微调后的模型利用测试集数据进行对比。
- 说明
- Embedding-distillatioin整体步骤
- 说明
- 模型其实就是bi-encoder,即模型输出embedding,然后计算QA相似度。
- 用KL散度将Qwen3-Embedding-8B向量大模型知识蒸馏给小模型BGE-m3 - 知乎
- generate_logits
- 针对原始数据中每一行中的 一个query、多个positive文档、多个negative文档 分别输入 Qwen3-embedding模型,得到向量。
- 针对数据形式
(query,positiveDoc,[negativeDoc,negativeDoc2,...]),分别计算query和每个doc的余弦相似度,然后将 所有的相似度 视为一个概率分布。
- train.sh
- 对于学生模型同样产生一个概率分布,和老师模型的分布计算KL损失,进而训练学生模型。
- 评估
- 利用预留的测试集进行评估。
- 说明
- uv 缓存管理问题
- 因为 项目目录和 uv缓存目录不在同一个文件系统下,所以uv缓存无法高效运转,只能只用硬复制来构建venv环境。所以需要迁移uv缓存到同一个文件系统下。
- 清理缓存项目,uv管理。
du -sh ~/.cache/uv .venv可以用来统计 venv是否真正多占用了存储。- uv 缓存迁移的时候 不能使用 mv 和 cp命令,必须使用
rsync -av命令,不然无法保持 软硬链接的关系。 - uv 缓存 通过 .bashrc 环境变量来指定
export UV_CACHE_DIR=/mnt/fast-ssd/uv-cache - 上述完成之后,项目的uv构建速度大幅提升至<1秒。
- 因为 项目目录和 uv缓存目录不在同一个文件系统下,所以uv缓存无法高效运转,只能只用硬复制来构建venv环境。所以需要迁移uv缓存到同一个文件系统下。
- CrossEncoder
- Cross-Encoders — Sentence Transformers documentation
- Cross-Encoder achieve higher performance than Bi-Encoders, however, they do not scale well for large datasets. Here, it can make sense to combine Cross- and Bi-Encoders, for example in Information Retrieval / Semantic Search scenarios: First, you use an efficient Bi-Encoder to retrieve e.g. the top-100 most similar sentences for a query. Then, you use a Cross-Encoder to re-rank these 100 hits by computing the score for every (query, hit) combination.
prompt格式
# prompt格式
message = [
{"role": "system", "content": "Judge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be \"yes\" or \"no\"."},
{"role": "user", "content": f"<Instruct>: {instruction}\n<Query>: {query}\n<Document>: {doc}"}
]
#LLM采样参数,只允许 yes 和 no 选项的logpro输出。
# 定义固定的 token 和采样参数
true_token = tokenizer("yes", add_special_tokens=False).input_ids[0]
false_token = tokenizer("no", add_special_tokens=False).input_ids[0]
sampling_params = SamplingParams(
temperature=0,
max_tokens=1,
logprobs=20,
allowed_token_ids=[true_token, false_token],
)
reranker蒸馏评估结果
# reranker 蒸馏训练之后的评估结果
(rag-optimization-practices) root@dsw-1457330-845c588466-dwlc9:/mnt/workspace/large_model_demo/RAG/RAG-Level-02/P03-RAG-Optimization-Practices/Reranker-Distillation# bash evaluate.sh
正在从 data/test.jsonl 加载数据集...
加载完成!共 2992 条样本。
--- 正在加载并评估模型: /mnt/workspace/modelscope/BAAI/bge-reranker-v2-m3 ---
--- 正在加载并评估模型: ./output/checkpoint-1217 ---
==================================================
✅ 最终评估结果汇总
==================================================
【蒸馏前】模型性能:
- MAP: 0.472061
- MRR@10: 0.478234
- NDCG@10: 0.547284
【蒸馏后】模型性能:
- MAP: 0.564523
- MRR@10: 0.573106
- NDCG@10: 0.638634
==================================================
🚀 性能变化分析 (蒸馏后 vs. 蒸馏前)
==================================================
指标 [MAP]:
- 绝对提升: +0.092462
- 相对提升: +19.59% ↑
指标 [MRR@10]:
- 绝对提升: +0.094872
- 相对提升: +19.84% ↑
指标 [NDCG@10]:
- 绝对提升: +0.091349
- 相对提升: +16.69% ↑
评估完成!✨
embedding蒸馏评估结果
(rag-optimization-practices) root@dsw-1457330-845c588466-dwlc9:/mnt/workspace/large_model_demo/RAG/RAG-Level-02/P03-RAG-Optimization-Practices/Embedding-Distillation# bash evaluation.sh
============================================================
模型性能对比评估
============================================================
------------------------------
领域内数据集评估
------------------------------
正在加载模型: /mnt/workspace/modelscope/BAAI/bge-m3
正在加载数据集: data/dataset_scidocs/test.jsonl
成功加载 3978 条样本
开始评估...
Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 63/63 [00:06<00:00, 9.34it/s]
Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1854/1854 [03:29<00:00, 8.85it/s]
正在加载模型: output/checkpoint-477
正在加载数据集: data/dataset_scidocs/test.jsonl
成功加载 3978 条样本
开始评估...
Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 63/63 [00:06<00:00, 9.28it/s]
Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1854/1854 [03:34<00:00, 8.63it/s]
------------------------------
领域外数据集评估
------------------------------
正在加载模型: /mnt/workspace/modelscope/BAAI/bge-m3
正在加载数据集: data/dataset_stackoverflowdupquestions/test.jsonl
成功加载 2992 条样本
开始评估...
Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 47/47 [00:04<00:00, 10.51it/s]
Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1398/1398 [01:56<00:00, 12.03it/s]
正在加载模型: output/checkpoint-477
正在加载数据集: data/dataset_stackoverflowdupquestions/test.jsonl
成功加载 2992 条样本
开始评估...
Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 47/47 [00:04<00:00, 10.15it/s]
Batches: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1398/1398 [01:57<00:00, 11.88it/s]
============================================================
领域内数据集 性能对比
============================================================
指标 蒸馏前 蒸馏后 绝对变化 相对变化(%)
------------------------------------------------------------
map 0.7744 0.8516 +0.0773 +9.98
mrr@10 0.9321 0.9548 +0.0227 +2.43
ndcg@10 0.8296 0.8956 +0.0660 +7.96
============================================================
领域外数据集 性能对比
============================================================
指标 蒸馏前 蒸馏后 绝对变化 相对变化(%)
------------------------------------------------------------
map 0.5166 0.5034 -0.0132 -2.56
mrr@10 0.5238 0.5110 -0.0127 -2.43
ndcg@10 0.5902 0.5778 -0.0124 -2.10
评估完成
问题:对于一个batch内的数据,有很多条数据有共同的前缀,那么vLLM中有针对这种情况的优化推理策略
是的!vLLM 确实针对「batch 内多条请求具有共同前缀(shared prefix)」的场景提供了专门的优化策略,这项技术称为:
✅ Prefix Caching(前缀缓存) 或 Prompt Sharing 这是 vLLM 的核心优化之一,能显著节省显存、提升吞吐、降低延迟,尤其适用于以下场景:
- RAG(检索增强生成):多个文档共享相同的 prompt 模板
- 多轮对话系统:历史上下文相同,仅用户最新输入不同
- 批量推理:如
“总结以下文本:<text>”,其中<text>不同但指令相同
🔍 工作原理
1. KV Cache 共享
- LLM 推理中,Prefill 阶段会为 prompt 生成 KV Cache(Key/Value 缓存)
- 如果多个请求的 prompt 前缀完全相同,它们的 KV Cache 前缀部分也完全相同
- vLLM 会:
- 只计算一次该公共前缀的 KV Cache
- 多个请求共享这份缓存
- 后续各自不同的部分独立计算
📌 本质:避免重复计算和存储相同的上下文
2. 与 PagedAttention 协同
- vLLM 使用 PagedAttention 管理 KV Cache(类似操作系统的虚拟内存分页)
- 公共前缀的 KV Cache 被存储在共享物理页中
- 每个请求的逻辑块(logical blocks)指向这些共享页 + 自有页
Request 1: [Shared Prefix] + [Suffix A]
Request 2: [Shared Prefix] + [Suffix B]
Request 3: [Shared Prefix] + [Suffix C]
→ KV Cache:
Shared Pages: [K/V of "Shared Prefix"]
Private Pages: [K/V of "Suffix A"], [Suffix B], [Suffix C]
🚀 性能收益
| 指标 | 优化效果 |
|---|---|
| 显存占用 | ↓ 显著减少(公共部分只存一份) |
| Prefill 计算量 | ↓ 减少(公共前缀只计算一次) |
| 吞吐(throughput) | ↑ 提升 2–5 倍(实测常见) |
| 支持 batch size | ↑ 可容纳更多请求 |
💡 实测案例(RAG 场景):
- 10 个请求,共享 2K token 的 prompt 模板
- 显存节省 >60%,吞吐提升 3.2 倍
🛠 如何启用?(vLLM 中默认开启!)
- 无需额外配置:vLLM 默认自动检测并启用 Prefix Caching
- 条件:
- 多个请求的 prompt 前缀必须字节级完全相同
- 使用相同的模型和参数(temperature 等不影响)
示例(Python client):
from vllm import LLM, SamplingParams
llm = LLM(model="Qwen/Qwen-7B-Chat")
**共享前缀:"你是一个 helpful AI。请回答:"**
prompts = [
"你是一个 helpful AI。请回答:巴黎是哪个国家的首都?",
"你是一个 helpful AI。请回答:量子力学是谁提出的?",
"你是一个 helpful AI。请回答:Python 如何读取文件?"
]
outputs = llm.generate(prompts, SamplingParams(temperature=0.7))
**→ vLLM 自动共享前缀的 KV Cache!**
⚠️ 注意事项
| 问题 | 说明 |
|---|---|
| 前缀必须完全一致 | 包括空格、标点、换行符。"你好" ≠ "你好 " |
| 不支持动态插入 | 如果前缀在运行时拼接(如加时间戳),则无法共享 |
| 与 LoRA 兼容性 | 若使用不同 LoRA adapter,即使 prompt 相同也无法共享 |
| 仅限 Prefill 阶段 | Decode 阶段因输出不同,无法共享 |
🔧 高级用法:手动控制(vLLM ≥ 0.4.0)
可通过 enable_prefix_caching=True 显式启用(默认已开):
llm = LLM(
model="meta-llama/Llama-3-8b",
enable_prefix_caching=True # 默认 True
)
也可通过环境变量:
VLLM_ENABLE_PREFIX_CACHING=1
🆚 与其他系统的对比
| 系统 | 是否支持前缀共享 |
|---|---|
| vLLM | ✅ 是(基于 PagedAttention) |
| HuggingFace TGI | ❌ 否(每个请求独立 KV Cache) |
| TensorRT-LLM | ✅ 是(需手动构建 shared context) |
| SGLang | ✅ 是(RadixAttention 机制) |
💡 vLLM 的实现是自动、透明、高效的代表。
✅ 总结
- vLLM 支持 Prefix Caching,自动优化共享前缀的 batch 请求
- 无需代码修改,只要 prompt 前缀相同即可生效
- 显著节省显存 + 提升吞吐,特别适合 RAG、批量指令等场景
- 确保你的 prompt 模板严格一致(避免尾部空格/换行差异)
如果你正在做 RAG 或批量推理,Prefix Caching 是 vLLM 给你的一份免费性能大礼包!🎉
问题:但是一个batch内的数据是一起进行并行推理的呀,那么这个时候 cache还没产生吧?
这是一个非常关键且深刻的问题!你的直觉是对的:如果整个 batch 是“同时”进行 prefill(即并行处理所有 prompt),那似乎还没生成 KV Cache,怎么共享? 但实际情况是:vLLM 的 Prefix Caching 并不是在“batch 内部并行时”动态发现共享的,而是在调度阶段通过“前缀树(Radix Tree / Trie)”预先组织请求,使得相同前缀的请求在逻辑上被识别并复用已缓存的 KV 块。 下面我们一步步澄清这个看似矛盾的问题:
✅ 核心答案
Prefix Caching 的“cache”并不是来自当前 batch 内部的其他请求,而是来自:
- 之前已完成的请求(跨 batch 共享),或
- 在当前 batch 内部,vLLM 调度器会先处理公共前缀一次,再广播给其他请求(逻辑上串行化前缀计算) 但在 纯首次 batch(没有任何历史 cache)且所有请求首次出现 的情况下,同一个 batch 内的请求确实无法彼此共享 KV Cache —— 除非 vLLM 显式地将公共前缀提取出来单独计算一次。 而 vLLM 正是这么做的!
🔍 深入机制:vLLM 如何在单个 batch 内实现前缀共享?
关键技术:Radix Tree(基数树) + Block-wise KV Cache Management vLLM 在调度一个 batch 前,会做以下事情:
步骤 1:构建 Radix Tree
- 将 batch 中所有 prompt 插入一棵 前缀树(Trie)
- 自动识别最长公共前缀(LCP)
Prompts:
"你是AI。Q: 巴黎?"
"你是AI。Q: 量子?"
"你是AI。Q: Python?"
→ Radix Tree:
root
└── "你是AI。Q: "
├── "巴黎?"
├── "量子?"
└── "Python?"
步骤 2:分层 Prefill(Hierarchical Prefill)
- 先对公共前缀
"你是AI。Q: "执行一次 prefill- 生成其对应的 KV Cache blocks
- 再对每个后缀(“巴黎?” 等)分别执行 prefill
- 每个后缀的 attention 计算会 拼接公共前缀的 KV blocks + 自身后缀的 KV blocks
🎯 这相当于 把一个长 prompt 拆成 “shared part + unique part”,并在计算时组合。
步骤 3:PagedAttention 支持非连续 KV 块
- 每个请求的完整 KV Cache = [shared_block_ids] + [unique_block_ids]
- Attention 计算时,PagedAttention 自动从多个物理页读取,对模型透明
📌 重要澄清
| 误解 | 事实 |
|---|---|
| “batch 内所有 prompt 同时并行计算” | ❌ 实际是:公共前缀先算一次,后缀再并行(逻辑上分阶段) |
| “cache 必须来自历史请求” | ❌ 即使全是新请求,vLLM 也能在 batch 内部构造共享 |
| “Prefill 是完全独立的” | ❌ vLLM 的调度器会主动重组计算图以启用共享 |
🧪 实验验证(思想实验) 假设 batch 有 3 个全新请求,无任何历史 cache:
prompts = [
"Hello world A",
"Hello world B",
"Hello world C"
]
vLLM 的执行流程:
- 调度器发现公共前缀
"Hello world " - 创建一个虚拟“共享请求”,prefill
"Hello world "→ 得到 KV blocks S - 对每个后缀
"A","B","C":- 构造输入:
"Hello world X" - 但 KV Cache = S + prefill(“X”) 的结果
- 构造输入:
- 最终每个请求的输出正确,且 S 只计算了一次
💡 这本质上是一种 计算图融合优化,由 vLLM 的 scheduler 自动完成。
📚 官方依据
- vLLM 论文(vLLM: Easy, Fast, and Cheap LLM Serving)提到:
“PagedAttention enables sharing of memory blocks across requests with common prefixes.”
- 源码中
RadixAttention(后续演进)和PrefixCacher组件明确支持 intra-batch prefix sharing
✅ 总结
| 问题 | 回答 |
|---|---|
| 同一个 batch 内的请求能共享前缀吗? | ✅ 能!即使全是新请求 |
| cache 是哪里来的? | 来自 当前 batch 内部对公共前缀的首次计算(调度器主动提取) |
| 是否需要历史请求? | ❌ 不需要(跨 batch 共享是额外 bonus) |
| 是否破坏并行性? | ⚠️ 公共前缀计算是串行的,但后缀仍可并行,整体吞吐仍大幅提升 |
💡 所以你的观察很敏锐,但 vLLM 通过 智能调度 + Radix Tree + PagedAttention 解决了这个“鸡生蛋”问题。 这正是 vLLM 相比传统推理引擎(如 HF Transformers)的先进之处!
P04 rag-best-practices
项目原理和结果
参考项目 chaoql/rag-best-practices: 大模型检索增强生成技术最佳实践。
- Qdrant的代码组织非常糟糕。
- QdrantVectorStore 里面还套了一个fastembed模型。
- 对于模型下载模块的问题定义了独立的缓存目录,但是又和hf的目录混到了一块。并且函数接口写得很不通用。
- 所以放弃这块功能的跑通。
- 原理
- 所谓混合检索就是快慢结合,fastembed(sparse embed) 和denseembed
- HyDE
- Hypothetical Document Embeddings(HyDE) 是一种提升检索质量的高级嵌入技术,由 Gao et al. 在 2022 年提出(论文:Precise Zero-Shot Dense Retrieval without Relevance Labels)。它的核心思想是:
- 不直接用用户查询(query)去检索,而是先让大语言模型(LLM)生成一个“假设性答案”(hypothetical document),再用这个假设答案的嵌入向量去检索真实文档。
- Hypothetical Document Embeddings(HyDE) 是一种提升检索质量的高级嵌入技术,由 Gao et al. 在 2022 年提出(论文:Precise Zero-Shot Dense Retrieval without Relevance Labels)。它的核心思想是:
- 这个项目中的hybrid 模式的意思是,query生成一个稀疏向量、一个稠密向量,两种同时请求数据库,而稀疏向量可能是按照传统倒排索引来进行索引构建的,即最终既有 倒排索引搜索 又有向量搜索。
- 当然Qrant 本身也允许请求带 filter,即关键词检索、类别检索等。
- embedding 模型微调过程
- 对于输入数据文件中的每一条数据,通过chatglm模型,构造prompt(其中包含一条文档),要求模型返回一些和上下文相关的问题,即形成了Answer-Question 对。
- 然后反转A-Q对(映射关系为n-to-1),构造出微调数据。
- 总体来说,就是依赖一个超大的chat大模型和一个文档数据集,构造出Q-A相似对,进而将chat大模型的对该文档的相关知识(对文档数据集领域的知识理解更透彻、更精细) 微调到一个emb小模型内。
- 模型微调使用的是llama-index 中 embedding 模型微调的功能类
llama_index.finetuning.embeddings.sentence_transformer.SentenceTransformersFinetuneEngine。- 其中损失函数是 MultipleNegativesRankingLoss
- MultipleNegativesRankingLoss 对于本文的数据形式 即 Q-A对,即将A视作一个正样本,同时采样 in-batch内的其他样本作为多个负样本,来计算出一个 Softmax Cross-Entropy Loss,然后再Q维度上取均值即为batch损失。
- Configuring the Response Mode
- 模型下载
- HuggingfaceEmbedding 类 如何解决外网不可访问的问题。
- 使用 modelscope下载模型,然后制定 HuggingfaceEmbedding的模型加载路径为 modelscope缓存中模型的绝对路径。(不是模型缓存的父目录,而是具体到一个模型的目录)
- modelscope上的模型不全,有些知名度不高的模型 hf上游,但modelscope上缺少。
- 服务器
- 阿里云服务器可以使用 modelscope 来进行下载,然后然后模型加载都使用绝对路径来加载。
- 本地开发
- 可以使用 huggingface 来进行下载和管理。
- FlagEmbeddingReranker
- “Flag” in FlagEmbeddingReranker = BAAI(智源研究院)的开源模型品牌标识,意为“旗舰级嵌入/重排模型”,无特殊技术含义,主要是项目命名的一部分。
- modelscope的模型目录架构和huggingface的模型目录结构 差异很大,不能直接复用。所以如果要加载本地模型,务必使用模型绝对路径。
- HuggingfaceEmbedding 类 如何解决外网不可访问的问题。
- 工程
- Qdrant
- 高级过滤能力: 支持结合向量相似性搜索和任意 Payload 结构化数据过滤(称为 Hybrid Search)。
- node_parser
- llama_index.core.node_parser.text.sentence_window.SentenceWindowNodeParser 按照句子进行切割,多个句子作为一个索引文档。
- Node Parser Modules | LlamaIndex Python Documentation
- pickle方式不安全是因为
__reduce__方法中可以定义一个 指令字节流,在反序列化的时候可以触发对应指令的执行。
- Qdrant
问题:pytorch_model.bin 和 model.safetensors 分别是什么
pytorch_model.bin 和 model.safetensors 是两种常见的用于存储深度学习模型权重(参数)的文件格式,广泛用于 Hugging Face Transformers 等生态中。它们的核心目的相同:保存神经网络的可训练参数,但在设计哲学、安全性、性能和兼容性方面有显著区别。
1. pytorch_model.bin
是什么?
- 这是 PyTorch 原生的模型权重保存格式。
- 本质上是一个通过
torch.save()保存的 Python pickle 文件,通常包含一个字典,键为参数名(如"transformer.h.0.attn.c_attn.weight"),值为对应的torch.Tensor。
特点:
| 方面 | 说明 |
|---|---|
| 格式 | 基于 Python 的 pickle 序列化 |
| 兼容性 | 只能在 PyTorch 环境中加载(需匹配版本) |
| 安全性 ❗ | 不安全:pickle 可执行任意代码,加载不受信任的 .bin 文件可能导致远程代码执行(RCE)漏洞 |
| 速度 | 加载/保存较快,但依赖 Python 反序列化 |
| 跨语言 | 不支持(强依赖 Python 和 PyTorch) |
| 典型用途 | Hugging Face 模型仓库中的默认权重格式(尤其旧模型) |
示例加载方式:
import torch
state_dict = torch.load("pytorch_model.bin", map_location="cpu")
model.load_state_dict(state_dict)
⚠️ 警告:永远不要加载来自不可信来源的
pytorch_model.bin!
2. model.safetensors
是什么?
- 由 Hugging Face 和 CarperAI 联合开发的一种安全、快速、框架无关的模型权重存储格式。
- 不使用
pickle,而是将张量数据以原始二进制形式存储,并附带 JSON 元数据描述张量结构。
特点:
| 方面 | 说明 |
|---|---|
| 格式 | 自定义二进制格式 + JSON header(无代码执行) |
| 安全性 ✅ | 安全:仅包含数值数据,无法执行任意代码 |
| 速度 | 加载通常比 .bin 更快(尤其大模型),支持内存映射(mmap) |
| 跨框架 | 支持 PyTorch、TensorFlow、Flax、JAX 等(只要能读取 NumPy-like 张量) |
| 文件大小 | 与 .bin 几乎相同(未压缩,但可选 gzip) |
| 典型用途 | Hugging Face 新模型推荐格式,尤其在公开分享或 Web 服务中 |
文件结构简述:
[JSON metadata header (UTF-8, padded to 8-byte align)]
{
"weight1": {"dtype": "F32", "shape": [1024, 1024], "data_offsets": [0, 4194304]},
"weight2": {"dtype": "F16", "shape": [512], "data_offsets": [4194304, 4195328]}
}
[Raw binary tensor data (concatenated)]
示例加载方式(PyTorch):
from safetensors.torch import load_file
state_dict = load_file("model.safetensors", device="cpu")
model.load_state_dict(state_dict)
对比总结
| 特性 | pytorch_model.bin |
model.safetensors |
|---|---|---|
| 底层机制 | Python pickle |
自定义二进制 + JSON |
| 安全性 | ❌ 危险(可执行代码) | ✅ 安全(纯数据) |
| 加载速度 | 快 | 通常更快(支持 mmap) |
| 跨平台/语言 | 否(PyTorch + Python) | 是(多框架支持) |
| Hugging Face 推荐 | 旧模型默认 | 新模型首选 |
| 文件扩展名 | .bin |
.safetensors |
实际建议
- ✅ 优先使用
.safetensors:尤其当你从网上下载模型、部署到生产环境或提供 API 服务时。 - 🔒 避免加载未知来源的
.bin文件:除非你完全信任提供者。 - 🔄 很多模型仓库同时提供两种格式(如 Llama 3、Mistral),你可以自由选择。
- 🛠️ 可用工具转换:
# 使用 Hugging Face transformers 自动转换 from transformers import AutoModel model = AutoModel.from_pretrained("your-model", use_safetensors=True) # 自动优先加载 .safetensors
如果你正在开发一个需要加载外部模型的应用(如 Web UI、推理服务),强烈建议只支持 .safetensors 格式以规避安全风险。
问题:Sparse text embeddings 和 dense text embeddings 的区别
Sparse text embeddings 和 dense text embeddings 是两种不同的文本表示方法,它们在结构、生成方式、应用场景和性能特点上有显著区别。以下是详细对比:
一、核心定义
| 类型 | 定义 |
|---|---|
| Dense Embeddings(稠密嵌入) | 将文本映射为一个低维(通常几百到几千维)、连续、稠密的实数向量,几乎所有维度都有非零值。 |
| Sparse Embeddings(稀疏嵌入) | 将文本表示为一个高维(可能数万到百万维)、离散、稀疏的向量,绝大多数维度为 0,只有少数维度有非零值(通常是正权重)。 |
二、典型代表
| 类型 | 模型/方法示例 |
|---|---|
| Dense | - BERT, RoBERTa - Sentence-BERT (SBERT) - BGE (BAAI General Embedding) - OpenAI Embeddings - Cohere Embeddings |
| Sparse | - BM25(传统,非学习型) - SPLADE - uniCOIL - DeepImpact - ColBERT(部分稀疏,带后期交互) |
💡 注意:BM25 虽然常被归为“稀疏检索”,但它不是“embedding 模型”(无神经网络),而 SPLADE 等是基于深度学习的稀疏 embedding 模型。
三、结构对比(举例) 假设词汇表大小为 30,000:
- Sparse embedding(如 SPLADE):
→ 每个非零位置对应一个词(如 “apple” 在索引 12345),值表示重要性。
[0, 0, 0, ..., 2.1, 0, 0, ..., 0.8, ..., 0] # 长度=30,000,仅几十个非零值 - Dense embedding(如 BGE):
→ 向量维度与词汇表无关,语义信息分布在整个向量中。
[0.23, -0.41, 0.88, ..., 0.05] # 长度=1024,几乎每个值都非零
四、关键区别总结
| 维度 | Sparse Embeddings | Dense Embeddings |
|---|---|---|
| 维度 | 高维(≈词汇表大小,如 30k–1M) | 低维(通常 384–4096) |
| 非零元素 | 极少(< 100 个) | 几乎全部非零 |
| 可解释性 | ✅ 高(每个非零维对应一个词) | ❌ 低(语义分布在隐空间) |
| 存储效率 | ✅ 高(可用倒排索引压缩存储) | ⚠️ 中等(需存储完整浮点数组) |
| 检索方式 | 倒排索引(Inverted Index) + TF-IDF/BM25 风格打分 | 向量相似度(如余弦、内积)+ ANN(如 FAISS, HNSW) |
| 训练目标 | 优化词级重要性(如通过 KL 散度对齐 BM25) | 优化句子/段落级语义相似度(如对比学习) |
| 多语言支持 | 依赖词汇表,跨语言较难 | 更容易通过共享语义空间实现 |
| 硬件加速 | CPU 友好,适合大规模文档库 | GPU/ANN 加速效果更好 |
五、应用场景
✅ Sparse 更适合:
- 大规模文档检索(如搜索引擎):==利用成熟的倒排索引技术==,高效过滤海量文档。
- 关键词敏感任务:需要精确匹配术语(如法律、医疗检索)。
- 资源受限环境:无需 GPU,纯 CPU 即可高效运行。
✅ Dense 更适合:
- 语义相似度任务:理解同义、 paraphrase、上下文含义(如 “car” ≈ “automobile”)。
- 小样本或 zero-shot 场景:泛化能力强。
- RAG 系统中的精排:与 LLM 结合效果好。
六、混合方法(Hybrid Retrieval) 现代系统常结合两者优势:
Query → [Sparse Retriever (BM25/SPLADE)] → Top K candidates
+ [Dense Retriever (BGE)] → Top K candidates
→ 合并去重 → Reranker (e.g., BGE-reranker)
- ColBERT:虽输出稠密 token 向量,但通过 MaxSim 实现类似稀疏的细粒度匹配。
- BGE-M3(BAAI 最新模型):同时输出 dense、sparse 和 multi-vector 表示,支持统一检索。
七、可视化比喻
- Sparse:像一本索引目录——只列出关键词及其页码(位置+权重)。
- Dense:像一幅抽象画——整体表达语义,无法拆解单个元素含义。
总结一句话:
Sparse embeddings = 关键词加权(可解释、高效);Dense embeddings = 语义压缩(泛化强、需向量检索)。 根据你的任务需求(精度 vs 效率、关键词匹配 vs 语义理解),可以选择合适类型,或直接使用 hybrid 方案获得最佳效果。
问题:Sparse Embeddings的模型如何做到 输出稀疏向量的
这是一个非常好的问题!Sparse Embeddings 模型(如 SPLADE、uniCOIL、DeepImpact 等)之所以能输出“稀疏但有信息量”的向量,关键在于它们通过神经网络学习每个词项(term)的重要性权重,并强制绝大多数权重为零或接近零。下面从原理、技术手段和训练目标三个方面详细解释:
一、核心思想:将文本映射到“词汇表维度”的稀疏向量
与 dense embedding 输出一个固定低维向量不同,sparse embedding 模型的输出维度 = 词汇表大小(V)(例如 V = 30,000)。
对于输入文本,模型会为词汇表中的每个词 $w_i$ 预测一个 非负重要性分数 $s_i \geq 0$,最终输出向量为:
$$ \mathbf{e} = [s_1, s_2, …, s_V] $$
其中 绝大多数 $s_i = 0$,只有少数关键词的 $s_i > 0$,形成稀疏表示。
✅ 这个向量可以直接用于 倒排索引(Inverted Index)检索,就像 BM25 一样高效。
二、如何实现“稀疏性”?关键技术手段
1. 使用带稀疏诱导的激活函数 最常用的是 ReLU + 对数变换 + FLOPS 正则化(以 SPLADE 为代表):
(1)基于 BERT 的 token-level logits
- 输入文本 → BERT 编码器 → 得到每个 token 的上下文表示。
- 通过一个线性层(或 MLM head)预测 整个词汇表上每个词的 logits(类似掩码语言建模)。
(2)应用 $\text{ReLU}(\log(1 + \exp(x)))$ 或直接 $\text{F}(x) = \max(0, x)$
- 将 logits 经过 非负激活,确保输出 ≥ 0。
- 实际中常用:
$$s_i = \text{ReLU}\left( \text{MLM logits}_i \right)$$ 或更平滑的: $$ s_i = \log\left(1 + \exp(\text{MLM logits}_i)\right) \quad \text{(softplus)} $$
📌 注意:即使 logits 是稠密的,后续会通过正则化让大多数 $s_i \to 0$。
2. 引入稀疏性正则化(Sparsity Regularization) 这是实现“真正稀疏”的关键!模型在训练时会惩罚非零元素的数量或大小。
常见正则项(加在 loss 中):
- L1 正则化:$\lambda \sum_i |s_i|$
→ 鼓励权重趋近于 0。 - FLOPS 正则化(SPLADE 提出):
$$ \mathcal{L}_{\text{flops}} = \left( \frac{1}{V} \sum_i \mathbb{E}[s_i] \right)^2 $$ → 惩罚平均激活强度,促使模型只激活极少数词。 - Group Lasso / Top-k 约束:强制只保留 top-k 个最大权重。
💡 训练完成后,推理时可设置阈值(如 $s_i < 1e-4$ 则置 0),进一步提升稀疏度。
3. 利用预训练语言模型的 MLM Head
- SPLADE 等模型直接复用 BERT 的 Masked Language Model (MLM) 头。
- MLM 头本来就能预测“哪些词可能出现在上下文中”,天然具有词级语义感知能力。
- 通过微调 MLM 头,使其输出的 logits 不再用于预测被遮盖词,而是作为全文的词重要性分布。
✅ 这样既利用了预训练知识,又实现了端到端的稀疏表示学习。
三、训练目标:对齐相关性信号 为了让稀疏向量“有信息量”(即能有效排序相关文档),模型通常采用以下监督信号:
| 方法 | 训练目标 |
|---|---|
| SPLADE | 最大化查询-正样本文档的相似度(内积),最小化查询-负样本的相似度 + 稀疏正则 |
| DeepImpact / uniCOIL | 回归到伪标签(如 BM25 分数、教师模型打分) |
| 无监督 SPLADE | 使用对比学习或自监督(如文档-标题对) |
| 相似度计算方式: $$ \text{score}(q, d) = \sum_{i=1}^V \text{ReLU}(q_i) \cdot \text{ReLU}(d_i) $$ → 本质是 加权词重叠(Weighted Term Matching),但权重由神经网络学习得到,比 TF-IDF/BM25 更智能。 |
四、举个具体例子:SPLADE 如何工作?
- 输入查询
"How to bake a cake?" - 经过 BERT + MLM head → 输出 30,000 维 logits
- 应用 $\text{ReLU}(\cdot)$ → 得到非负分数
- 加入 FLOPS 正则 → 模型学会只给
"bake","cake","recipe"等词高分,其余为 0 - 最终输出稀疏向量:仅几十个非零值,每个对应一个关键词及其语义重要性 🔍 检索时:
- 文档也用同样方式编码成稀疏向量
- 通过倒排索引快速计算内积得分(只计算非零维度交集)
五、为什么叫“响亮”(informative)?
- 虽然稀疏,但非零维度是模型精心挑选的关键词,且权重反映其在当前上下文中的语义重要性。
- 相比 BM25 的静态 TF-IDF,sparse embedding 能:
- 理解同义词(“car” → 激活 “automobile”)
- 抑制停用词(“the”, “how” 权重≈0)
- 根据上下文动态调整(“apple” 在科技 vs 水果语境下激活不同词)
总结
| 关键点 | 说明 |
|---|---|
| 输出维度 | = 词汇表大小(高维) |
| 稀疏性来源 | ReLU/Softplus 激活 + L1/FLOPS 正则化 |
| 信息量来源 | 基于预训练 MLM 头 + 相关性监督学习 |
| 检索兼容性 | 完全兼容传统倒排索引系统(如 Lucene, Elasticsearch) |
| 代表模型 | SPLADE(最经典)、uniCOIL、DeepImpact、BGE-M3(支持 sparse 输出) |
✅ 所以,“稀疏但响亮” = 只保留最关键的几个词,但每个词的权重都经过深度语义理解优化。 如果你正在构建检索系统,sparse embeddings 能让你在不牺牲太多语义能力的前提下,享受传统搜索引擎的高效与可扩展性。
其他问题
问题:langchain中的竖线运算符是由哪些方法来实现的
在 langchain 中,竖线运算符(|)的实现主要依赖于以下方法:
核心实现方法
__or__方法- 这是 Python 中实现竖线运算符的核心魔术方法
- 当使用
a | b语法时,Python 会调用a.__or__(b)
__ror__方法- 右侧对象的反向或运算方法
- 当左侧对象没有实现
__or__时调用
在 Langchain 中的应用
在你的代码中 prompt|llm|output_parser 这个链条中:
- prompt(
ChatPromptTemplate对象)实现了__or__方法 - llm(
ChatOpenAI对象)也实现了__or__方法 - output_parser(
StrOutputParser对象)同样支持链式操作
实现机制
**伪代码示例**
class BaseComponent:
def __or__(self, other):
return Chain(self, other)
这种设计模式允许不同的 langchain 组件(如 prompt、llm、output_parser)通过竖线运算符组合成一个完整的处理链 chain。
问题:langchain中的StrOutputParser 是如何自动对 chatbot的结果进行渲染的
作为一名资深技术导师,我来详细解释 LangChain 中的 StrOutputParser 不是如何“自动渲染”结果的,而是它在 LangChain 表达式语言 (LCEL) 管道中扮演的关键角色。
核心观点:StrOutputParser 并非渲染器,而是转换器
StrOutputParser 的作用不是对结果进行复杂的“渲染”(如格式化为 Markdown 或 HTML),而是执行一个非常简单但至关重要的类型转换:
它确保管道(Chain)的最终输出是标准的 Python 字符串(str)类型。
在 LangChain 管道中,“渲染”这个词通常意味着将输出从一个复杂对象转换为用户或下一个组件可以使用的格式。
1. 为什么需要 StrOutputParser?
LLM 模型的原始输出通常是一个复杂的对象,而不是一个简单的字符串:
| 原始输出类型 | 解释 |
|---|---|
BaseMessage |
对于 Chat 模型 (ChatOpenAI, ChatGroq 等),它们的 invoke() 或 stream() 方法返回的是一个 BaseMessage 对象(通常是 AIMessage 或 HumanMessage 的子类)。 |
Generation |
对于 Completion 模型 (OpenAI 等),它们的 invoke() 返回的是一个 Generation 对象的列表(包装在 LLMResult 中)。 |
StrOutputParser 的工作就是从这些复杂的对象中,安全地提取出人类可读的字符串内容。 |
2. StrOutputParser 的工作机制
StrOutputParser 在 LCEL 管道中实现了 BaseOutputParser 接口,它的核心逻辑非常简单:
A. 处理 Chat 模型输出 (BaseMessage 对象)
当它接收到 AIMessage 或其他 BaseMessage 对象时,它会调用该对象的 .content 属性。
- 输入示例:
<AIMessage content='你好!我是一个AI。'> - 输出结果:
'你好!我是一个AI。'
B. 处理 Completion 模型输出 (LLMResult / Generation 对象)
如果它接收到的是 Completion 模型返回的结构,它会提取第一个 Generation 对象的文本内容。
C. 处理流式输出 (Streaming)
在流式传输中,StrOutputParser 会持续接收到片段(Chunks)的 BaseMessage 对象或字符串。它的作用是:
- 提取内容: 从每个传入的 Chunk 中提取
.content属性。 - 拼接内容: 将这些内容片段安全地拼接起来,形成最终的完整字符串。这确保了前端或用户接收到的流是连续且正确的文本。
3. 如何“自动渲染”的误解
用户之所以觉得它是“自动渲染”的,是因为它将一个不透明的 LangChain 内部对象(如 AIMessage)转换成了应用程序前端可以直接显示的 Python str 类型。
在 LCEL 管道中,StrOutputParser 通常位于管道的末端,因为它确保了管道的输出能够被后续的系统(如 Flask API 响应、Streamlit 界面或简单的 print() 语句)轻松消费。
总结
StrOutputParser 是 LangChain 管道中一个类型转换组件,它的作用是:
- 从 LLM 返回的复杂对象(如
AIMessage)中提取纯文本内容。 - 确保 Chain 的输出类型始终是标准的
str,以便于后续处理和展示。 - 在流式传输中,负责将接收到的内容片段安全拼接。
问题:那么streamlit的write方法是如何将markdown格式文本自动渲染为有格式的html的
代码 streamlit.elements.write.WriteMixin.write 中有讲解这部分内容。
这是一个非常好的问题,它触及了 Streamlit 的核心机制和前端交互原理。
streamlit.write() 方法自身不会执行 Markdown 渲染,它是一个“万能”命令,会根据传入的数据类型进行分派(Dispatch)。当它接收到字符串时,它会将渲染工作委托给专门的函数。
下面是 st.write() 如何将 Markdown 文本自动转换为 HTML 格式化显示的详细过程。
1. st.write() 的分派机制 (The Swiss Army Knife)
st.write() 被称为 Streamlit 的“瑞士军刀”,它会检查传入的参数类型:
| 传入参数类型 | st.write() 实际调用 |
|---|---|
str (字符串) |
委托给 st.markdown() |
pd.DataFrame |
委托给 st.dataframe() |
dict 或 list |
委托给 st.json() |
Altair/Plotly Chart |
委托给相应的图表函数 |
Exception |
委托给 st.exception() |
因此,当你传入一个包含 Markdown 格式的字符串时,实际执行渲染的是 st.markdown() 函数。 |
2. st.markdown() 的核心渲染过程
st.markdown() 是专门负责将 Markdown 文本转换为 HTML 的函数。这个转换过程在 Streamlit 内部是这样运作的:
A. 后端(Python)处理:使用 Markdown 解析库
- 解析器使用: Streamlit 的 Python 后端不会自己编写 Markdown 转换逻辑。它会依赖一个或多个成熟的、高性能的 Python Markdown 解析库(例如,早期版本可能依赖
markdown-it-py或类似的库)。 - 生成 HTML/抽象结构: 当你调用
st.markdown(body)时,body字符串会被这个内部的 Markdown 解析器处理,并转换成对应的 HTML 结构(或一个包含渲染指令的中间数据结构)。- 例如,
**bold**被转换为<strong>bold</strong>。
- 例如,
- 序列化与传输: Streamlit 将这个 HTML 结构或渲染指令序列化(通常为 JSON 格式)。
- WebSocket 传输: 序列化后的数据通过 WebSocket 连接,从 Python 后端发送到 Streamlit 的前端(浏览器)。
B. 前端(浏览器)渲染:React + CSS
- 前端接收: Streamlit 的前端(使用 React 框架构建)接收到通过 WebSocket 发送过来的指令/数据。
- DOM 操作: React 组件根据收到的 HTML 结构或指令,将其安全地插入到 Web 页面的 DOM (Document Object Model) 中。
- 浏览器渲染: 浏览器识别这些 HTML 标签(如
<h1>,<p>,<strong>)并应用默认或 Streamlit 自定义的 CSS 样式。- 样式应用: Streamlit 具有内置的样式表,确保所有的 Markdown 元素(标题、列表、代码块等)都遵循应用的主题和外观,看起来美观且一致。
3. 为什么是自动的? 对于开发者来说,整个过程是自动且无感的,因为:
- 封装:
st.write()和st.markdown()封装了从 Markdown 字符串到 HTML 渲染的所有步骤。 - 实时同步: Streamlit 的架构本身就是为了在 Python 后端代码与浏览器前端之间提供实时同步。当你调用一个 Streamlit 命令时,Python 框架会立即处理数据并告诉前端如何更新。
问题:Pydantic 中 BaseModel 的核心作用:数据模型和数据校验
在 Python 的 pydantic 库中,BaseModel 是构建数据模型的基石。它的主要作用是定义数据的结构、类型,并自动进行验证。
简单来说,BaseModel 将松散的 Python 字典数据提升为具有明确结构、类型和内置验证的强大数据对象。
🚀 BaseModel 的主要功能
1. 定义清晰的数据结构 (Schema Definition)
BaseModel 允许你使用标准的 Python 类型注解来定义你的数据对象应该长什么样,包括哪些字段、每个字段的类型。
- 示例:
Python
这个类清楚地定义了一个用户对象必须包含
from pydantic import BaseModel from datetime import datetime class User(BaseModel): id: int name: str = "John Doe" # 带有默认值 signup_ts: datetime | None = None # 联合类型 is_active: boolid(整数)、name(字符串)、signup_ts(日期时间或 None)和is_active(布尔值)。
2. 自动数据校验 (Data Validation)
这是 BaseModel 最核心的功能。当你尝试创建一个 BaseModel 实例时,Pydantic 会自动检查传入的数据是否符合你定义的类型和结构。
- 如果数据类型正确: 它会将输入数据(比如 JSON 字符串、字典)转换为正确的 Python 类型(例如将 JSON 字符串 “1” 转换为整数 1)。
- 如果数据校验失败: 它会抛出一个清晰的
ValidationError,告诉你哪个字段出了问题,以及为什么。这极大地减少了运行时错误和调试时间。
3. 自动数据转换 (Data Coercion)
BaseModel 不仅进行校验,还尝试进行合理的类型转换:
- 字符串到数字: 字符串
"123"会被自动转换为整数123。 - 字符串到日期/时间: 遵循 ISO 格式的日期字符串会被自动转换为
datetime对象。
4. 强大的序列化和反序列化
BaseModel 实例内置了将数据对象转换为其他格式的方法:
- 序列化 (Serialization):
user_instance.model_dump(): 将模型实例转换为标准的 Python 字典 (dict)。user_instance.model_dump_json(): 将模型实例转换为 JSON 字符串,方便网络传输。
- 反序列化 (Deserialization):
User(**data_dict): 从字典创建模型实例。User.model_validate(data_dict): 从字典或 JSON 数据中验证并创建模型实例。
5. 与 FastAPI 等框架集成
在 FastAPI 中,BaseModel 是定义请求体 (Request Body) 和 响应体 (Response Body) 的标准方式。
- 当用作请求体时,FastAPI 自动使用
BaseModel来校验客户端发送的 JSON 数据。 - 当用作响应体时,FastAPI 自动使用
BaseModel来序列化你的 Python 对象为 JSON 响应。 总结:BaseModel的作用就是将 Python 应用程序中的数据处理从松散、易错的字典操作,升级为结构化、类型安全且自带验证的强大对象模型。
问题:llama2并非是专属的embedding模型,也可以用OllamaEmbeddings来加载么?
这是一个非常专业和关键的问题,它涉及到大模型的多功能性和 Ollama 平台的灵活性。
答案是:是的,您可以将 Llama 2 (或 Llama 3 等聊天/生成模型) 用作 OllamaEmbeddings 的模型,但它并非最佳实践。
以下是详细的解释和建议:
1. 为什么 Llama 2 可以用于嵌入?(技术可行性)
Ollama 平台提供了一个通用的 /api/embeddings REST API 端点。
当您在 OllamaEmbeddings 中指定一个非专用的聊天模型(如 llama2)时,Ollama 守护进程会执行以下操作:
- 提取隐藏状态 (Hidden State): Ollama 会运行 Llama 2 模型,但不执行最终的**语言模型头(LM Head)**层(该层负责预测下一个词元)。
- 池化 (Pooling): 它会从模型的倒数第二层(或根据配置的特定层)提取词元隐藏状态(Token Hidden States)。然后,它通常会应用一个池化策略(如平均池化)将这些词元向量聚合为一个单一的句子向量,作为最终的嵌入(Embedding)。
LangChain 的
OllamaEmbeddings通过调用这个通用的 Ollama API,实现了对 Llama 2 的嵌入功能调用。
2. 为什么 Llama 2 不推荐用于嵌入?(效果考量) 尽管技术上可行,但 Llama 2 或其他聊天模型(如 Mistral)在没有经过特定微调的情况下,作为嵌入模型的效果通常远不如专用嵌入模型。
| 嵌入模型类型 | Llama 2 (生成模型) | 专用嵌入模型 (如 Nomic Embed, mxbai-embed-large) |
|---|---|---|
| 训练目标 | 生成文本(预测下一个词元)。 | 语义相似性(训练目标是让语义相近的句子在向量空间中距离更近)。 |
| 效果/精度 | 较差。 向量空间没有针对相似性搜索进行优化,检索精度低。 | 优秀。 在 MTEB 等基准测试中表现优异,RAG 效果好。 |
| 资源消耗 | 高。 Llama 2 尺寸较大(如 7B),推理慢,内存占用高。 | 低。 专用模型通常更小(如 300M),速度快,资源消耗低。 |
| 简而言之: Llama 2 生成的向量可能无法准确捕捉不同句子之间的语义相似性,这会严重损害您的 RAG 检索质量。 |
3. 架构师建议 作为一名资深架构师,我的建议是:
- 对于 RAG 或语义搜索: 始终优先使用专用的嵌入模型。 您已经看到了,Ollama 支持许多高性能、小巧且专门为嵌入任务优化的模型(如
mxbai-embed-large或embeddinggemma)。这能提供更好的精度和更快的速度。 - 实验和自定义: 如果您只是进行实验,或者您的 Llama 2 是经过专门的 对比学习(Contrastive Learning)微调版本(例如,
AnglE-LLaMA等),那么使用 Llama 2 是有意义的。在这种情况下,你需要确保你的 Llama 2 模型文件在 Ollama 中已经包含了必要的嵌入配置。 结论:使用OllamaEmbeddings(model="llama2")是可行的,但为了您的应用性能和准确性,请使用一个专用的 Ollama 嵌入模型。