文章总结: 本文介绍基于LangChain框架的文档问答系统实现,重点讲解如何使用向量模型处理CSV格式安全文档。通过HuggingFaceEmbeddings将文本转化为向量,利用余弦相似度检索相关内容,提供完整的环境配置、代码示例及中英文模型效果对比。关键发现显示向量相似度反映上下文关联性而非语义属性,并给出使用中文模型优化本地化处理的实用建议。 综合评分: 81 文章分类: WEB安全,安全工具,安全开发,AI安全,数据安全
Langchain4_基于文档问答
原创
羽泪云小栈 羽泪云小栈
羽泪云小栈
2026年4月7日 10:00 陕西
基于文档的问答
有时候我们想通过检索pdf、txt、csv等文档,让llm帮忙总结内容等… 需要向量的参与
https://www.bilibili.com/video/BV1b4421D71Y/?spm_id_from=333.788.videopod.episodes&vd_source=c49e37118f69c7a5b34915b73d1b78ab&p=5
https://github.com/ConnectAI-E/LangChain-Tutior/blob/main/python/cn/5.%E6%96%87%E6%A1%A3%E9%97%AE%E7%AD%94.ipynb
需要两个模型噢,一个向量模型(可以本地解决),一个对话模型
前置准备
https://docs.langchain.com/oss/python/integrations/vectorstores/docarray_in_memory
pip install -qU langchain-community "docarray"
我需要重配置一下虚拟环境,不然下载库的时候有冲突
python -m venv langchain_env
1.langchain_env\Scripts\activate
或者
2.在VSCode中,不需要手动激活:
按 Ctrl+Shift+P
输入 Python: Select Interpreter
选择你的虚拟环境:E:\Langchain_Learn\langchain_env\Scripts\python.exe
打开新的cmd终端,VSCode会自动激活虚拟环境
虚拟环境下:
pip install langchain langchain-openai langchain-community docarray dotenv pandas
右键运行时会报错,找不到文件,还是要新建文件夹为.vscode,该目录下新建文件settings.json
配置为:
{
// Python 解释器配置
"python.defaultInterpreterPath": "E:\\Langchain_Learn\\langchain_env\\Scripts\\python.exe",
// 终端自动激活虚拟环境
"python.terminal.activateEnvironment": true,
"python.terminal.activateEnvInCurrentTerminal": true,
// Code Runner 配置
"code-runner.executorMap": {
"python": "cd $dir && E:\\Langchain_Learn\\langchain_env\\Scripts\\python.exe $fileName"
},
"code-runner.runInTerminal": true,
"code-runner.fileDirectoryAsCwd": true,
"code-runner.saveFileBeforeRun": true,
"code-runner.clearPreviousOutput": false,
"code-runner.respectShebang": false,
"code-runner.ignoreSelection": true,
// 文件编码配置
"files.encoding": "utf8",
"files.autoSave": "afterDelay",
// 终端配置
"terminal.integrated.defaultProfile.windows": "PowerShell",
"terminal.integrated.profiles.windows": {
"PowerShell": {
"source": "PowerShell",
"icon": "terminal-powershell"
}
}
}
相当于每次用的是虚拟环境的python.exe
csv准备
准备好本次的文档,让llm生成下即可
import pandas as pd
from io import StringIO
import os
from langchain_community.document_loaders import CSVLoader
file='cybersecurity_qa.csv'
loader=CSVLoader(file_path=file,encoding="utf-8")
docs = loader.load()
print(docs[0])
"""
page_content='title: SQL注入攻击原理
content: SQL注入是一种将SQL代码插入或添加到应用(用户)的输入参数中的攻击技术,攻击者通过这些参数传递给后台
SQL服务器加以解析并执行。常见的注入方式包括:联合查询注入、布尔盲注、时间盲注、报错注入等。防御措施:使用参
数化查询、输入验证、最小权限原则。
category: Web安全' metadata={'source': 'cybersecurity_qa.csv', 'row': 0}
向量
https://zhuanlan.zhihu.com/p/634237861 嵌入(embedding)和向量(vector)
很像做机器学习的时候的特征提取,将通用的数据用向量表示其含义。
这里的向量也不是以前学数学的那种平面向量,机器学习里,是一个有序的数字列表,理解为一种编码的方式吧,将事物编码成向量,模型就能够学习、计算和推理。
要用到向量模型,而不是对话模型。
比如:from langchain_openai import OpenAIEmbeddings
Embedding是将文本,变为一组数字(向量),这组数字就代表它所表示的文本含义。
内容相似的文本,也有相似的向量值;内容不同的文本,向量值的区别也大。
所以这种技术是利用向量找到和问题相似的文本片段,一起传递给llm回答问题。
用了本地免费向量模型HuggingFaceEmbeddings
#这是旧版
from langchain_community.embeddings import HuggingFaceEmbeddings
#新版推荐安装pip install -U langchain-huggingface 我下不下来,就算了
pip install sentence-transformers #embeddings需要这个
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com' #第一次使用时要下载的,超时就用镜像站
import pandas as pd
from io import StringIO
import os
from langchain_community.document_loaders import CSVLoader
from langchain.indexes import VectorstoreIndexCreator
from langchain_community.embeddings import HuggingFaceEmbeddings
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
file='cybersecurity_qa.csv'
loader=CSVLoader(file_path=file,encoding="utf-8")
docs = loader.load()
#print(docs[0])
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
embed = embeddings.embed_query("Web安全")
print(len(embed))
print(embed[:5])
384
[-0.09801744669675827, 0.014501313678920269, -0.014877825044095516, -0.008558204397559166, -0.017776664346456528]
这个意思是说,它有384个维度的向量值
来三个例子,直观感受一下,并用余弦相似度比较向量之间是否相似
import pandas as pd
from io import StringIO
import os
from langchain_community.document_loaders import CSVLoader
from langchain.indexes import VectorstoreIndexCreator
from langchain_community.embeddings import HuggingFaceEmbeddings
from sklearn.metrics.pairwise import cosine_similarity
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
file='cybersecurity_qa.csv'
loader=CSVLoader(file_path=file,encoding="utf-8")
docs = loader.load()
#print(docs[0])
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
text1="SQL注入是一种数据库攻击技术"
text2="SQL注入可以窃取数据库中的敏感数据"
text3="上传shell可以获取权限"
embed1 = embeddings.embed_query(text1)
print(len(embed1))
print(embed1[:10])
embed2 = embeddings.embed_query(text2)
print(len(embed2))
print(embed2[:10])
embed3 = embeddings.embed_query(text3)
print(len(embed3))
print(embed3[:10])
vectors = [embed1,embed2,embed3]
sim_matrix = cosine_similarity(vectors)
print("\n相似度矩阵:")
print(sim_matrix)
都是384个维度,然后相似度矩阵就是二维数组嘛
文本跟自身比那肯定完全一致。
可以看出文本1和文本2 是比较相似的。为0.74
文本1和文本3,以及文本2和文本3不太相似。因为小于0.5了都
其实也取决于模型本身的采用过的数据集啊、训练方式等等吧,为了严谨一点,用一个支持中文的模型(也支持中英混合)
https://hf-mirror.com/BAAI/bge-small-zh-v1.5
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
embeddings = HuggingFaceBgeEmbeddings(
model_name="BAAI/bge-small-zh-v1.5"
)
text1="SQL注入是一种数据库攻击技术"
text2="SQL注入可以窃取数据库中的敏感数据"
text3="上传shell可以获取权限"
总结一下:
llm通过嵌入向量(embedding vectors)达到目的
就是我们并不指望llm能看懂文字,实际上它也不需要懂。
只需要将文本转化为向量值,也就是一组数字,用这组数字来代表文本含义。
这样一来,问题就被转化成了:问题文本对应一个向量,而 CSV 文件里的每条内容也对应各自的向量。
接下来要做的,就是找出与问题向量最相似的其它向量。找到了,也就找到了想要的答案
特别说明,有时候两个差别很大的词,对应的向量也被认为是相似的,不是因为它们属性相同,而是因为它们在大量文本中总是一起出现(比如”苹果电脑”),模型把”上下文使用场景的相似”当作了”相似”
某种方面来讲:向量相似度 ≠ 属性相似度,而是”文本搭配/上下文环境的相似度”
用英文的模型试试:苹果和电脑的关联度
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
text1="apple"
text2="pear"
text3="computer"
[[1. 0.47442532 0.5624204 ]
[0.47442532 1. 0.35905761]
[0.5624204 0.35905761 1. ]]
看,apple和computer的相似度 比另外两个相互搭配的确实要高一些,为0.5624204…
中文模型呢
embeddings = HuggingFaceBgeEmbeddings(
model_name="BAAI/bge-small-zh-v1.5"
)
text1="苹果"
text2="梨子"
text3="电脑"
[[1. 0.58180949 0.66143068]
[0.58180949 1. 0.39980222]
[0.66143068 0.39980222 1. ]]
这里苹果和电脑的相似度为0.66,也会高一些,当然,取决于预训练的模型质量等等…
索引查询
VectorstoreIndexCreator 一键创建”向量存储索引”,算是一键完成了所有步骤。(文档加载、分割文本、创建向量存储、创建检索器、链)
先用英文的向量模型试试
import os
from dotenv import load_dotenv, find_dotenv
from langchain_openai import ChatOpenAI
from langchain_community.document_loaders import CSVLoader
from langchain_community.vectorstores import DocArrayInMemorySearch
import pandas as pd
from langchain_openai import OpenAIEmbeddings
from langchain.indexes import VectorstoreIndexCreator
from langchain_community.embeddings import HuggingFaceEmbeddings
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
load_dotenv(find_dotenv())
llm = ChatOpenAI(
# This is the default and can be omitted
api_key=os.environ.get("KEY2"),
base_url=os.environ.get("base_url2"),
temperature=0.0,#让预测不让那么随机,偏平稳
model=os.environ.get("model2")
)
file='cybersecurity_qa.csv'
loader=CSVLoader(file_path=file,encoding="utf-8")
#data = pd.read_csv(file,header=None)
#print(data)
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
index = VectorstoreIndexCreator(
embedding=embeddings,
vectorstore_cls=DocArrayInMemorySearch
).from_loaders([loader])
query="""请列举出所有属于Web安全类型的title和content,并对它们每一个进行总结"""
#使用索引查询创建一个响应,并传入这个查询
response = index.query(query,llm=llm)
print(response)
毕竟是本地模型,理解一下,其实本来有3个,能找到2个已经不错了。
换成中文模型:
from langchain_community.embeddings import HuggingFaceEmbeddings,HuggingFaceBgeEmbeddings
...
embeddings = HuggingFaceBgeEmbeddings(
model_name="BAAI/bge-small-zh-v1.5"
)
...
#如果我的query仍然是:
query1="""请列举出所有属于Web安全类型的title和content,并对它们每一个进行总结"""
#结果依然只有2个,但是如果改为:
query2="""请列举出所有category为Web安全的title和content,并对它们每一个进行总结"""
3条,一个不少。这就和平时与llm_chat聊天一样的,prompt等这些措辞,还是需要重视的。
当然这也与向量模型本身有关,因为上面那个英文模型我用了同样的query2,依然只有2条…
向量数据库
用于存储生成的向量。只用于存储
DocArrayInMemorySearch
def from_documents(
cls: Type[VST],
documents: List[Document],
embedding: Embeddings,
**kwargs: Any,
) -> VST:
"""Return VectorStore initialized from documents and embeddings.
Args:
documents: List of Documents to add to the vectorstore.
embedding: Embedding function to use.
kwargs: Additional keyword arguments.
Returns:
VectorStore: VectorStore initialized from documents and embeddings.
...
docs=loader.load()
#print(docs[0]) 每一行变成了一块一块的文档
#print(len(docs))
embeddings = HuggingFaceBgeEmbeddings(
model_name="BAAI/bge-small-zh-v1.5"
)
db = DocArrayInMemorySearch.from_documents(
docs,
embeddings
)
query="""请列举出所有category为Web安全的title和content,并对它们每一个进行总结"""
#使用向量存储来查找与query类似的文本
docs = db.similarity_search(query)
print(len(docs))
for i inrange(len(docs)):
print(f"第{i}个doc:{docs[i]}")
...
居然多了一个安全合规的doc,分别是原csv的第1、2、7、0行 (按数组那种索引来,第一行索引为0)
那个DocArrayInMemorySearch会把docs每一行作为小文档转化为向量存进去,并与问题转化的向量进行比较,可以这样看:
import os
from dotenv import load_dotenv, find_dotenv
from langchain_community.document_loaders import CSVLoader #文档加载器,采用csv格式存储
from langchain_community.embeddings import HuggingFaceEmbeddings,HuggingFaceBgeEmbeddings
from sklearn.metrics.pairwise import cosine_similarity
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
load_dotenv(find_dotenv())
file='cybersecurity_qa.csv'
loader=CSVLoader(file_path=file,encoding="utf-8")
#data = pd.read_csv(file,header=None)
#print(data)
docs=loader.load()
print(docs[0].page_content) #每一行变成了一块一块的文档
print(len(docs))
embeddings = HuggingFaceBgeEmbeddings(
model_name="BAAI/bge-small-zh-v1.5"
)
texts=[1,2,7,0,3,4,5,6,8,9]
vectors=[]
vectors.append(embeddings.embed_query("请列举出所有category为Web安全的title和content,并对它们每一个进行总结"))
for i in texts:
embed = embeddings.embed_query(docs[i].page_content)
vectors.append(embed)
'''
print(len(embed))
print(embed[:10])
'''
print(len(vectors))
sim_matrix = cosine_similarity(vectors)
print("\n相似度矩阵:")
print(sim_matrix)
#只取第一行,看着11个值,即问题向量与自己以及其它10个文档向量的相似度关系
[1. 0.609897530.575278710.570846850.56437030.49878579
0.528779440.540898650.528550720.55662580.49817377]
所以db.similarity_search(query)它的顺序结果为什么是1,2,7,0,那就是按相似度由高到低排的,它只取了前4个,也就是大于0.55的,而后面的3,4,5,6,8,9行相对而言相似度小一点,嗯,它默认返回前4个最相似的,k=4
(method) def similarity_search(
query: str,
k: int = 4,
**kwargs: Any
) -> List[Document]
Return docs most similar to query.
Args:
query: Text to look up documents similar to.
k: Number of Documents to return. Defaults to 4.
Returns:
List of Documents most similar to the query.
这里算是完成了两步,文档加载和向量存储,这里不需要文本分割,因为内容比较少,就贴个简单的代码实现吧
...
text_splitter = CharacterTextSplitter(chunk_size=1000)
split_docs = text_splitter.split_documents(docs)
db = DocArrayInMemorySearch.from_documents(
split_docs,
embeddings
)
...
检索器-回答文档问题
用到RetrievalQA链(实际上新版没啦,过时了,但后面知道要组合链就行了)
代码1-手动实现检索
先看下下面整个代码,不需要用到retriever:
...
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
load_dotenv(find_dotenv())
llm = ChatOpenAI(
# This is the default and can be omitted
api_key=os.environ.get("KEY2"),
base_url=os.environ.get("base_url2"),
temperature=0.0,#让预测不让那么随机,偏平稳
model=os.environ.get("model2")
)
file='cybersecurity_qa.csv'
loader=CSVLoader(file_path=file,encoding="utf-8")
#data = pd.read_csv(file,header=None)
#print(data)
docs=loader.load()
embeddings = HuggingFaceBgeEmbeddings(
model_name="BAAI/bge-small-zh-v1.5"
)
db = DocArrayInMemorySearch.from_documents(
docs,
embeddings
)
#合并docs所有页面内容到一个变量中
qdocs = "".join([docs[i].page_content for i inrange(len(docs))])
print(f"qdocs:{qdocs}")
query="""请列举出所有category为Web安全的title和content,并对它们每一个进行总结"""
response = llm.invoke(f"{qdocs} 问题: {query}")
print(response)
结果是一样的,不贴图了。
这个已经可以实现目的了,后面用链,说白了是以后好拓展些什么…看着也舒服些…
代码2-检索链
通用的链就是 chain=prompt | llm | parser
但是因为检索链需要的是先检索,再把检索结果放进prompt里,所以先看看下面这个
...
retriever=db.as_retriever()
result=retriever.invoke("什么是SQL注入")
print(result)
...
这一步retreiver在做什么,就是在检索,把与问题的向量值相似度高的内容返回出来而已
组合成链,本质就是:要求llm根据这些文档,回答我的问题
...
retriever=db.as_retriever()
prompt="""
请基于下面的内容,回答我的问题{input}:\n{content}
"""
prompt=ChatPromptTemplate.from_template(prompt)
qachain=(
{"content":retriever,"input":RunnablePassthrough()}
| prompt | llm
)
print(qachain.invoke("请列举出所有category为Web安全的title和content,并对它们每一个进行总结"))
...
一开始我感觉这样的写法是有些奇怪的。
qachain=(
{"content":retriever,"input":RunnablePassthrough()}
| prompt | llm
)
但是抓住这里的重心,也就是prompt,它构造了两个变量,content和input,也就是分别放我们的文档内容,和问题
而prompt数据要求的又是字典形式。
所以构造字典:{"content":retriever,"input":RunnablePassthrough()},这一步只是为了传递content这个变量,也就是把检索器赋值给了content,填充了一个变量值
那么input这个变量呢? 它不管,用RunnablePassthrough() “作为一个占位符”,表示动态获取输入(任何类型),也就是最终我们要提交的问题
qachain=(
RunnablePassthrough()|prompt|llm
)
query="这是一个问题"
print(qachain.invoke(
{"content":retriever,"input":query}
))
为什么不能这样,注意到retriever本身也是要invoke一次,也需要问题进行一次向量值比对的。
可以理解retriever是一个 Runnable( 一个”可执行单元”,接收输入,产生输出,可能改变,也可能不改变。),任何 Runnable 都有 .invoke() 方法
chain的组合,就是runnable的组合
如果是下面这种形式,是没意义的,它需要接收输入且能产生输出,{...}只是一个字典形式
{"content":content,"input":query} | prompt | llm
现在清晰了,{"content":retriever,"input":RunnablePassthrough(),这里有两个runnable,对于RunnablePassthrough(),接受输入,但原样输出的意义:
retriever.invoke("这是一个问题。") -> 返回值:是列表 比如[docs[0],docs[1]...]
RunnablePassthrough().invoke("这是一个问题。") ->返回值仍然是:"这是一个问题"
所以:两个 Runnable 各自独立接收同一个用户输入: 而不是所谓的链式传递,比如retriever.invoke(RunnablePassthrough()),此外,字典 Runnable 只能作为链的起点,而无法作为中间步骤
比如我之前的:这样是错误写法↓
chain1 | {"role": RunnablePassthrough()} | chain2
中间步骤只能用函数或 RunnableLambda
总结
总结一下,对于基于文档的问答,要用到两个模型,向量模型和对话模型,步骤为:文档加载、文本分割、创建向量存储、创建检索器、成链。直接用VectorstoreIndexCreator 简单些,但通过组合链的方式,多步骤可控会更灵活一些
注意
invoke(),是一种调用的方法… llm、chain、向量化等都可以这样用
RunnablePassthrough(),原样传递:输入是什么,输出就是什么,传个话的作用
https://reference.langchain.com/python/langchain-core/runnables/passthrough/RunnablePassthrough
#直接忽视警告下载
python -m pip install --ignore-installed langchain-huggingface
Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple
ERROR: Could not find a version that satisfies the requirement puccinialin (from versions: none)
ERROR: No matching distribution found for puccinialin
WARNING: You are using pip version 20.1.1; however, version 25.0.1 is available.
You should consider upgrading via the 'E:\Langchain_Learn\langchain_env\Scripts\python.exe -m pip install --upgrade pip' command.
#虚拟环境中
pip install --upgrade pip #如果当前cmd不是管理员模式,能把旧pip卸载,却因为权限问题,安装不了新pip,导致无pip模块可用
python -m ensurepip --upgrade #进行原pip恢复
干脆换官方源下载:
pip install --default-timeout=100 -i https://pypi.org/simple langchain-huggingface
ERROR: Could not find a version that satisfies the requirement puccinialin (from versions: none)
ERROR: No matching distribution found for puccinialin
WARNING: You are using pip version 20.1.1; however, version 25.0.1 is available.
You should consider upgrading via the 'e:\langchain_learn\langchain_env\scripts\python.exe -m pip install --upgrade pip' command.
但是puccinialin是3.9的,我的是3.8。
但是呢。。。
(虚拟环境中,管理员模式下)
e:\langchain_learn\langchain_env\scripts\python.exe -m pip install --upgrade pip 又成功升级了...
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:羽泪云小栈 羽泪云小栈 羽泪云小栈《Langchain4_基于文档问答》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论