Langchain4_基于文档问答

admin 2026-04-10 03:06:30 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍基于LangChain框架的文档问答系统实现,重点讲解如何使用向量模型处理CSV格式安全文档。通过HuggingFaceEmbeddings将文本转化为向量,利用余弦相似度检索相关内容,提供完整的环境配置、代码示例及中英文模型效果对比。关键发现显示向量相似度反映上下文关联性而非语义属性,并给出使用中文模型优化本地化处理的实用建议。 综合评分: 81 文章分类: WEB安全,安全工具,安全开发,AI安全,数据安全


cover_image

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_基于文档问答》

RSAC2026创新沙盒决赛回顾 网络安全文章

RSAC2026创新沙盒决赛回顾

文章总结: 本文回顾了RSAC2026创新沙盒决赛入围企业技术方案,涵盖AI智能体安全治理、大模型安全防护、人因工程反欺诈、代码安全自动化及智能运营等前沿领域。
评论:0   参与:  0