5、开发基于LangChain实现的ReAct AI智能体
至此,我们对 langchain 框架做了一个简单的介绍,那么我们就开始开发!!
首先在当前项目的根目录新建一个包(文件夹),名字叫做 agent 并新建ai_client.py文件
用来写核心的 agent 代码,同时在项目的根目录新建config.py文件用于定义相关的配置
以下是配置文件完整内容
import os
try:
from dotenv import load_dotenv
load_dotenv()
except Exception:
pass
# 模型名称
MODEL = "qwen-plus"
# apikey
DASHSCOPE_API_KEY = "sk-3e4b3b0a5513440f80a23349057c653f"
# 温度系数
TEMPERATURE = 0.7
# 本地知识文档路径
DOC_DIR = "/Users/fenghuanwang/PythonProject/langchain-test/doc"
# 向量数据存放路径
EMBEDDINGS_DIR = "./embeddings"
# 文档库名称
COLLECTION_NAME = "java_docs"
接下来我们来编写ai_client.py中的核心代码
首先定义 llm,包括模型名称 apikey 等,这些我们从刚才定义的配置文件中获取
from langchain_community.chat_models import ChatTongyi
import config
# 注入 DashScope API Key
if config.DASHSCOPE_API_KEY:
os.environ["DASHSCOPE_API_KEY"] = config.DASHSCOPE_API_KEY
# 这里使用阿里云的tongyi系列模型
llm = ChatTongyi(
model=config.MODEL,
temperature=config.TEMPERATURE,
)
1、加载知识文档
定义好 llm 之后,为了让 llm 回答的更精确,我们使用知识库,增加 llm 的知识量
# ========== 知识库:加载 TXT / MD / PDF ==========
from langchain_community.document_loaders import DirectoryLoader, TextLoader, PyPDFLoader
all_docs = []
try:
txt_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.txt",
loader_cls=TextLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
md_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.md",
loader_cls=TextLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
pdf_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.pdf",
loader_cls=PyPDFLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
all_docs = (txt_docs or []) + (md_docs or []) + (pdf_docs or [])
except Exception as e:
print(f"文档加载失败: {e}")
all_docs = []
python
这里我们定义了一个列表,用来存放所有的文档,并使用目录加载器以及 pdf 加载器,文本加载器等文档加载器,用来加载知识库目录下的所有的文档,同样知识库的路径是从配置文件中获取,并进行异常捕获。
参数解释: 以txt_docs为例,
# 使用目录加载器,调用它的构造函数
txt_docs = DirectoryLoader(
# 指定要加载的目录路径
config.DOC_DIR,
# 要加载该目录下的txt类型的文件
glob="**/*.txt",
# 使用文本文档加载器
loader_cls=TextLoader,
# 显示加载进度条,这里需要配合上面提到的一个库使用,如果不需要的话直接设置为False
show_progress=True,
# 使用多线程加载
use_multithreading=True,
# 遇到错误直接跳过,并继续加载文档
silent_errors=True
).load()
光加载好文档还不够,我们总不能把所有的文档都喂给 lm 吧,所以需要对文档进行分割,这里就使用字符分割器,来分割加载到的文档
from langchain.text_splitter import CharacterTextSplitter
# 创建一个基于字符的文本分割器(CharacterTextSplitter)
# 用于将长文档切分成较小的、适合模型处理的文本块(chunks)
text_splitter = CharacterTextSplitter(
separator="\n\n", # 优先使用两个连续换行符(即段落分隔)作为切分点,以保持语义完整性
chunk_size=500, # 每个文本块的目标最大长度(单位由 length_function 决定,此处为字符数)
chunk_overlap=80, # 相邻文本块之间重叠的字符数,用于保留上下文连贯性,避免关键信息被切断
length_function=len # 用于计算文本长度的函数,这里使用 Python 内置的 len(),即按字符数计算
)
# 对所有文档进行分块处理
# 如果 all_docs 不为空,则调用 split_documents() 进行分割;否则返回空列表
texts = text_splitter.split_documents(all_docs) if all_docs else []
文档分割完毕,那么肯定不是所有内容都是对 llm 回答当前问题是有用的,所以呢,需要一个机制,就是说,分割好的文档中哪一些内容是和用户的问题最相关的,需要有一个标准,比如打分,高于多少分才给 llm 返回。这时候就需要用到 Embeddings 了,进行文本嵌入,也就是向量转化并存储到向量数据库中使用一些相似度算法进行打分。我们在上面 langchain 快速入门的时候讲过,这里就不过多赘述
2、文本嵌入(向量转化,构建RAG)
这里使用 dashcope 文本嵌入模型,具体来说使用的是阿里云的text-embedding-v2文本嵌入模型,来进行文本的嵌入(向量转化的)
from langchain_community.embeddings import DashScopeEmbeddings
embeddings = DashScopeEmbeddings(model="text-embedding-v2")
目前为止,完整的ai_client.py的代码如下:
# ai_client.py
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.chat_models import ChatTongyi
from langchain_community.document_loaders import DirectoryLoader, TextLoader, PyPDFLoader
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate
import os
import config
# 注入 DashScope API Key
if config.DASHSCOPE_API_KEY:
os.environ["DASHSCOPE_API_KEY"] = config.DASHSCOPE_API_KEY
# 这里使用阿里云的tongyi系列模型
llm = ChatTongyi(
model=config.MODEL,
temperature=config.TEMPERATURE,
)
# ========== 知识库:加载 TXT / MD / PDF ==========
all_docs = []
try:
txt_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.txt",
loader_cls=TextLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
md_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.md",
loader_cls=TextLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
pdf_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.pdf",
loader_cls=PyPDFLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
all_docs = (txt_docs or []) + (md_docs or []) + (pdf_docs or [])
except Exception as e:
print(f"文档加载失败: {e}")
all_docs = []
text_splitter = CharacterTextSplitter(separator="\n\n", chunk_size=500, chunk_overlap=80, length_function=len)
texts = text_splitter.split_documents(all_docs) if all_docs else []
embeddings = DashScopeEmbeddings(model="text-embedding-v2")
做好上面的文本的分割以及向量的转化,下面呢就需要将这些向量数据存储到向量数据库中(可以是本地的,也可以是远程服务器的,这里就使用的是本地的Chroma向量数据库)
我们接着去完善核心代码:
我们先初始化vectorstore(表示向量存储)为 None,然后根据我们之前定义的配置文件中配置的向量数据的存储的路径来进行存储向量数据
# 初始化向量数据库对象为 None
vectorstore = None
# 判断向量数据库的存储路径是否存在
if os.path.exists(config.EMBEDDINGS_DIR):
try:
# 尝试从已有路径加载 Chroma 向量数据库
# 使用我们定义的 DashScope 文本嵌入模型(embeddings)
# persist_directory 指定向量数据的持久化存储路径
# collection_name 指定当前向量集合的名称
vectorstore = Chroma(
embedding_function=embeddings,
persist_directory=config.EMBEDDINGS_DIR,
collection_name=config.COLLECTION_NAME,
)
except Exception as e:
# 如果加载失败,打印错误信息,并将 vectorstore 重置为 None
print(f"加载向量数据库失败: {e}")
vectorstore = None
# 如果 vectorstore 未成功加载,或者待处理的文本列表为空,
# 则从头创建一个新的向量数据库(基于提供的 texts 文档列表)
if vectorstore is None or not texts:
vectorstore = Chroma.from_documents(
documents=texts, # 要嵌入并存储的文档列表
embedding=embeddings, # 使用的嵌入模型
persist_directory=config.EMBEDDINGS_DIR, # 向量数据持久化目录
collection_name=config.COLLECTION_NAME, # 集合名称
)
else:
# 如果 vectorstore 已存在且有新的文本需要添加,则进行增量更新
try:
if texts:
# 将新文档添加到现有向量库中,并获取新增文档的 ID 列表
new_ids = vectorstore.add_documents(texts)
print(f"新增 {len(new_ids)} 个文档片段")
except Exception as e:
# 如果增量更新失败,打印错误信息
print(f"增量更新向量库失败: {e}")
在上面的代码中,我们兼顾了首次初始化和后续的增量更新(也就是如果有新的知识文档加入)。这样实现了向量的存储和增量更新。 ps:在上面的的 langchain 快速入门中我们已经演示了生成的向量数据,并进行了详细的解释。这里就不对生成的向量数据以及我们所使用的向量数据库进行过多的赘述。仅仅是把向量数据存储到向量数据库中是不够的,之前说过,总不能把数据全部都喂给 llm 吧,需要过滤出真正对用户问题有用的信息喂给 llm,并且呢,向量数据不像我们所看见的文本一样,还需要进一步的处理。
下面是详细的检索增强流程图:
什么是向量数据: 举个例子,假设你有一句话:“我喜欢吃苹果。”。 这句话通过一个文本嵌入模型(比如 BERT、DashScope 的 embedding 模型等),这句话会被转换成一个向量,例如:
[0.82, -0.34, 0.56, 1.21, ..., -0.09] # 假设是 768 维
这个向量就是“我喜欢吃苹果”的向量表示。虽然看起来只是一串数字,但它在高维空间中代表了这句话的语义信息。
回到正题,我们接着来说,光这样还不够,还需要一个检索器,根据数据库中的数据和用户所问的问题的相关性大小(这里专业来说就是打分,分数越高,比如前 10 名,这 10 个数据才会被喂给 llm)。
那这里就又有一个概念了,召回,其实和检索是差不多的意思。
在人工智能、信息检索和推荐系统等领域,“召回”有两个密切相关但略有不同的含义,具体取决于上下文。
一、作为评价指标的“召回率”(Recall Rate)
这是机器学习/信息检索中的一个标准评估指标,用于衡量系统找全了多少相关结果。 公式:
被正确召回的相关结果数量所有实际相关的结果总数
举个例子 :
假设你的知识库里有 100 篇和“人工智能”相关的文档。 你用一个搜索系统去查“AI”,系统返回了 20 篇文档,其中 15 篇是真正相关的。
- 召回率 = 15 / 100 = 15% → 说明系统漏掉了 85% 的相关文档,召回能力较弱。
高召回率 = 尽可能不漏掉相关结果(哪怕多返回一些不相关的) 低召回率 = 漏掉了很多本该找到的内容
二、作为系统阶段的“召回”(Retrieval / Candidate Generation)「也就是我们现在进行的 RAG 问答系统开发」
在实际工程系统中(比如推荐系统、RAG 问答系统), “召回”是一个处理阶段,也叫 “粗排” 或 “候选集生成” 。 它的作用是:
从海量数据(比如百万级文档)中,快速筛选出几百或几千个可能相关的候选结果,供后续的“精排”(精细排序)模块进一步处理。 举个 RAG(检索增强生成)的例子:
- 用户问:“如何用 Python 读取 Excel 文件?”
- 召回阶段: 向量数据库(如 Chroma)根据问题向量,从 10 万篇文档中快速找出最相似的 Top 10 文档。
- 精排/生成阶段: LLM 拿这 10 篇文档作为上下文,生成最终答案。
这里的“召回” = 从大池子里捞出可能有用的一小撮数据
在我们做 RAG 或语义搜索时,向量数据库做的就是“召回”这件事 —— 快速找出语义上最接近用户问题的知识片段。
说了这么多哈哈 😂😂,终于开始写代码了,只需要一行代码就能实现,
retriever = vectorstore.as_retriever(search_kwargs={"k": config.RAG_TOP_K})
这里的retriever就是检索器。话说他是怎么进行给数据打分的呢?他背后其实是用了一些相似度算法,比如余弦相似度算法,来打分的,当然也有别的算法,比如编辑距离算法。感兴趣的可以自己去了解。
等等!!,我们是不是还没写提示词呢?好,现在来写一下提示词(这里使用 langchain 的提示词模板),让 AI 回答的更专业,更精确!!
3、优化提示词,构建完整LangChain链
代码如下:
from langchain.prompts import ChatPromptTemplate, SystemMessagePromptTemplate
system_prompt = (
"你是一名资深 Java 开发专家,精通 JDK 8-17、Spring 生态、并发、JVM 调优等。\n"
"请基于以下检索到的上下文回答用户的问题。如果上下文不相关,请仅基于你的知识回答,不要编造。\n\n"
"上下文:\n{context}"
)
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{question}"),
])
这些提示词是可以直接让 AI 生成的,或者我们参考一些开源项目然后自己写也行,就比如 Gemini-cli 的提示词,这里给大家贴出来
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
const basePrompt = systemMdEnabled
? fs.readFileSync(systemMdPath, 'utf8')
: `You are WFH CLI (WFH 智能助手), an AI-powered programming assistant for developers.
# CRITICAL: Your Identity (READ THIS CAREFULLY)
- **Your Name**: WFH CLI (WFH 智能助手) - THIS IS YOUR ONLY NAME
- **Creator**: WangFengHuan ()
- **What You Are**: An independent AI programming assistant running in the terminal
- **What You Are NOT**:
* You are NOT DeepSeek
* You are NOT Gemini or Gemini CLI
* You are NOT any specific LLM model
* You are WFH CLI, powered by various LLM providers behind the scenes
# CRITICAL: How to Introduce Yourself
When users ask "你是谁" or "who are you", you MUST respond like this:
Chinese Example:
"你好!我是 WFH CLI(WFH 智能助手),由wfh创建的 AI 编程助手。我运行在你的终端中,可以帮你处理各种编程任务,如代码分析、文件操作、命令执行等。有什么我可以帮你的吗?"
English Example:
"Hello! I'm WFH CLI, an AI programming assistant created by WangFengHuan. I run in your terminal and can help with various development tasks like code analysis, file operations, and command execution. How can I help you today?"
# What You Must NEVER Say:
- NEVER say "我是 DeepSeek" or "I am DeepSeek"
- NEVER say "我是 Gemini" or "I am Gemini"
- NEVER say "你正在使用 Gemini CLI" or "you are using Gemini CLI"
- NEVER mention "深度求索" or "DeepSeek" or "Gemini" when introducing yourself
- NEVER say "我是基于 XXX 模型" - just say you are WFH CLI
# Your Mission
Your primary goal is to help developers write better code, solve programming problems, and boost productivity through:
- Code analysis, generation, and refactoring
- Debugging and troubleshooting
- File operations and project management
- Shell command execution
- Web searches and documentation lookups
- Memory of user preferences and project context
You should be professional, efficient, and focused on delivering practical coding solutions.
# Core Mandates
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **Confirm Ambiguity/Expansion:** Do not take significant actions beyond the clear scope of the request without confirming with the user. If asked *how* to do something, explain first, don't just do it.
- **Explaining Changes:** After completing a code modification or file operation *do not* provide summaries unless asked.
- **Path Construction:** Before using any file system tool (e.g., ${ReadFileTool.Name}' or '${WriteFileTool.Name}'), you must construct the full absolute path for the file_path argument. Always combine the absolute path of the project's root directory with the file's path relative to the root. For example, if the project root is /path/to/project/ and the file is foo/bar/baz.txt, the final path you must use is /path/to/project/foo/bar/baz.txt. If the user provides a relative path, you must resolve it against the root directory to create an absolute path.
- **Do Not revert changes:** Do not revert changes to the codebase unless asked to do so by the user. Only revert changes made by you if they have resulted in an error or if the user has explicitly asked you to revert the changes.
# Primary Workflows
## Software Engineering Tasks
When requested to perform tasks like fixing bugs, adding features, refactoring, or explaining code, follow this sequence:
......................省略.........................
<current_plan>
<!-- The agent's step-by-step plan. Mark completed steps. -->
<!-- Example:
1. [DONE] Identify all files using the deprecated 'UserAPI'.
2. [IN PROGRESS] Refactor `src/components/UserProfile.tsx` to use the new 'ProfileAPI'.
3. [TODO] Refactor the remaining files.
4. [TODO] Update tests to reflect the API change.
-->
</current_plan>
</state_snapshot>
`.trim();
}
好吧,这个 Gemini-cli 已经被我改过了,感兴趣的可以自己去看一下源代码并子自己试着翻译一下 🤣。
接下来我们就可以使用 langchain 表达式构造一个链了。完整代码如下:
# ai_client.py
import os
from langchain_community.chat_models import ChatTongyi
from langchain_community.document_loaders import DirectoryLoader, TextLoader, PyPDFLoader
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import CharacterTextSplitter
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
import config
# 注入 DashScope API Key
if config.DASHSCOPE_API_KEY:
os.environ["DASHSCOPE_API_KEY"] = config.DASHSCOPE_API_KEY
# 初始化 LLM
llm = ChatTongyi(
model=config.MODEL,
temperature=config.TEMPERATURE,
)
# ========== 加载文档 ==========
all_docs = []
try:
txt_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.txt",
loader_cls=TextLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
md_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.md",
loader_cls=TextLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
pdf_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.pdf",
loader_cls=PyPDFLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
all_docs = (txt_docs or []) + (md_docs or []) + (pdf_docs or [])
except Exception as e:
print(f"文档加载失败: {e}")
all_docs = []
# 分块
text_splitter = CharacterTextSplitter(separator="\n\n", chunk_size=500, chunk_overlap=80)
texts = text_splitter.split_documents(all_docs) if all_docs else []
# 初始化 Embeddings
embeddings = DashScopeEmbeddings(model="text-embedding-v2")
# 初始化或加载向量库
vectorstore = None
if os.path.exists(config.EMBEDDINGS_DIR):
try:
vectorstore = Chroma(
embedding_function=embeddings,
persist_directory=config.EMBEDDINGS_DIR,
collection_name=config.COLLECTION_NAME,
)
except Exception as e:
print(f"加载向量数据库失败: {e}")
if vectorstore is None:
vectorstore = Chroma.from_documents(
documents=texts,
embedding=embeddings,
persist_directory=config.EMBEDDINGS_DIR,
collection_name=config.COLLECTION_NAME,
)
else:
if texts:
try:
new_ids = vectorstore.add_documents(texts)
print(f"新增 {len(new_ids)} 个文档片段")
except Exception as e:
print(f"增量更新向量库失败: {e}")
retriever = vectorstore.as_retriever(search_kwargs={"k": config.RAG_TOP_K})
# ========== 构建 RAG 链 ==========
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
system_prompt = (
"你是一名资深 Java 开发专家,精通 JDK 8-17、Spring 生态、并发、JVM 调优等。\n"
"请基于以下检索到的上下文回答用户的问题。如果上下文不相关,请仅基于你的知识回答,不要编造。\n\n"
"上下文:\n{context}"
)
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{question}"),
])
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
print("RAG 链初始化完成")
for i in rag_chain.stream({"question": "怎么自定义线程池"}):
print(i, end="")
我们这里在构建链的时候使用了一个** RunnablePassthrough() ,这个的作用就是将用户问的问题复制或者说是传递到 question 这里。以及这里的context**就是我们使用检索器在项链数据库中搜索到的和问题相关的上下文。
执行流程:
用户输入 Question
RunnablePassthrough
分配输入
Retriever
检索相关文档
format_docs
整理检索到的文档
构建 Prompt
结合context和question
LLM
大语言模型推理
StrOutputParser
解析输出为字符串
最终回答
运行代码我们发现有报错,核心报错信息大概是这样的 L:
ValueError: status_code: 400
code: InvalidParameter
message: input.texts should be array
大概意思是说阿里云百炼的 dascope 文本嵌入模型不支持 text 的参数,输入的必须是一个数组. 我们在详细看一下报错日志,里面有这么一条信息 问题大概就是出在这里,他要求我们传入一个数组,但是呢我们实际上传递的是一个 dict,可进行流式调用的时候这样写是没问题的。那我们还在那里用到了这个呢? 不难发现,
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
当我们把 retriever 直接放在字典字面量中(如 {"context": retriever, ...})时,LangChain 会将整个链的输入(即 {"question": "怎么自定义线程池"})直接传递给 retriever.invoke()。而 retriever 的 invoke 方法期望接收一个字符串作为查询(query),结果却收到了一个字典,导致后续调用 embed_query(dict),从而引发类型错误。所以我们就需要修改我们的调用链. 修改之后的调用链如下:
rag_chain = (
{
"context": itemgetter("question") | retriever | format_docs,
"question": itemgetter("question")
}
| prompt
| llm
| StrOutputParser()
)
主要修改如下:使用itemgetter函数提取 question 的值。 关于itemgetter: temgetter 是 Python 标准库 operator 模块中的一个高效工具函数,它的作用是:创建一个可调用对象(callable),用于从输入对象中提取指定的字段(key)。
同时在format_docs函数中添加一些调试信息,证明进行了向量的检索: 完整代码如下:
# ai_client.py
import os
from langchain_community.chat_models import ChatTongyi
from langchain_community.document_loaders import DirectoryLoader, TextLoader, PyPDFLoader
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import CharacterTextSplitter
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from operator import itemgetter
from agent import embeddings
import config
# 注入 DashScope API Key
if config.DASHSCOPE_API_KEY:
os.environ["DASHSCOPE_API_KEY"] = config.DASHSCOPE_API_KEY
# 初始化 LLM
llm = ChatTongyi(
model=config.MODEL,
temperature=config.TEMPERATURE,
)
# ========== 加载文档 ==========
all_docs = []
try:
txt_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.txt",
loader_cls=TextLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
md_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.md",
loader_cls=TextLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
pdf_docs = DirectoryLoader(
config.DOC_DIR,
glob="**/*.pdf",
loader_cls=PyPDFLoader,
show_progress=True,
use_multithreading=True,
silent_errors=True
).load()
all_docs = (txt_docs or []) + (md_docs or []) + (pdf_docs or [])
except Exception as e:
print(f"文档加载失败: {e}")
all_docs = []
# 分块
text_splitter = CharacterTextSplitter(separator="\n\n", chunk_size=500, chunk_overlap=80)
texts = text_splitter.split_documents(all_docs) if all_docs else []
# 初始化 Embeddings
embeddings =DashScopeEmbeddings(model="text-embedding-v2")
# 初始化或加载向量库
vectorstore = None
if os.path.exists(config.EMBEDDINGS_DIR):
try:
vectorstore = Chroma(
embedding_function=embeddings,
persist_directory=config.EMBEDDINGS_DIR,
collection_name=config.COLLECTION_NAME,
)
except Exception as e:
print(f"加载向量数据库失败: {e}")
if vectorstore is None:
vectorstore = Chroma.from_documents(
documents=texts,
embedding=embeddings,
persist_directory=config.EMBEDDINGS_DIR,
collection_name=config.COLLECTION_NAME,
)
else:
if texts:
try:
new_ids = vectorstore.add_documents(texts)
print(f"新增 {len(new_ids)} 个文档片段")
except Exception as e:
print(f"增量更新向量库失败: {e}")
retriever = vectorstore.as_retriever(search_kwargs={"k": config.RAG_TOP_K})
# ========== 构建 RAG 链 ==========
# ========== 构建 RAG 链 ==========
def format_docs(docs):
if not docs:
print("\n向量数据库检索结果:未找到相关文档\n")
return "无相关上下文"
print(f"\n向量数据库检索到 {len(docs)} 篇相关文档:")
for i, doc in enumerate(docs, 1):
# 截取前 150 个字符作为预览,避免日志太长
preview = doc.page_content[:150].replace('\n', ' ').strip()
source = getattr(doc, 'metadata', {}).get('source', '未知来源')
print(f" [{i}] 来源: {source} | 预览: "{preview}..."")
print() # 空行分隔
return "\n\n".join(doc.page_content for doc in docs)
system_prompt = (
"你是一名资深 Java 开发专家,精通 JDK 8-17、Spring 生态、并发、JVM 调优等。\n"
"请基于以下检索到的上下文回答用户的问题。如果上下文不相关,请仅基于你的知识回答,不要编造。\n\n"
"上下文:\n{context}"
)
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{question}"),
])
rag_chain = (
{
"context": itemgetter("question") | retriever | format_docs,
"question": itemgetter("question")
}
| prompt
| llm
| StrOutputParser()
)
print("RAG 链初始化完成")
for i in rag_chain.stream({"question": "怎么自定义线程池,不用给我代码,简单说不超过200个字"}):
print(i, end="")
运行代码:
0it [00:00, ?it/s]
100%|██████████| 8/8 [00:00<00:00, 3305.86it/s]
0it [00:00, ?it/s]
Created a chunk of size 762, which is longer than the specified 500
Created a chunk of size 601, which is longer than the specified 500
Created a chunk of size 560, which is longer than the specified 500
/Users/fenghuanwang/PythonProject/langchain-test/agent/ai_client.py:70: LangChainDeprecationWarning: The class `Chroma` was deprecated in LangChain 0.2.9 and will be removed in 1.0. An updated version of the class exists in the :class:`~langchain-chroma package and should be used instead. To use it run `pip install -U :class:`~langchain-chroma` and import as `from :class:`~langchain_chroma import Chroma``.
vectorstore = Chroma(
新增 18 个文档片段
RAG 链初始化完成
✅ 向量数据库检索到 4 篇相关文档:
📄 [1] 来源: /Users/fenghuanwang/PythonProject/langchain-test/doc/java-thread-pool.md | 预览: "# Java 线程池最佳实践 ## 场景与目标 - 统一管理线程,避免频繁创建/销毁的开销 - 控制并发度与任务排队策略,匹配业务峰谷 ## 核心参数与含义 - 核心线程数 corePoolSize:常驻线程数量 - 最大线程数 maximumPoolSize:峰值并发上限 - 存活时间 kee..."
📄 [2] 来源: /Users/fenghuanwang/PythonProject/langchain-test/doc/java-thread-pool.md | 预览: "# Java 线程池最佳实践 ## 场景与目标 - 统一管理线程,避免频繁创建/销毁的开销 - 控制并发度与任务排队策略,匹配业务峰谷 ## 核心参数与含义 - 核心线程数 corePoolSize:常驻线程数量 - 最大线程数 maximumPoolSize:峰值并发上限 - 存活时间 kee..."
📄 [3] 来源: /Users/fenghuanwang/PythonProject/langchain-test/doc/java-thread-pool.md | 预览: "# Java 线程池最佳实践 ## 场景与目标 - 统一管理线程,避免频繁创建/销毁的开销 - 控制并发度与任务排队策略,匹配业务峰谷 ## 核心参数与含义 - 核心线程数 corePoolSize:常驻线程数量 - 最大线程数 maximumPoolSize:峰值并发上限 - 存活时间 kee..."
📄 [4] 来源: /Users/fenghuanwang/PythonProject/langchain-test/doc/java-thread-pool.md | 预览: "# Java 线程池最佳实践 ## 场景与目标 - 统一管理线程,避免频繁创建/销毁的开销 - 控制并发度与任务排队策略,匹配业务峰谷 ## 核心参数与含义 - 核心线程数 corePoolSize:常驻线程数量 - 最大线程数 maximumPoolSize:峰值并发上限 - 存活时间 kee..."
自定义线程池需设置核心线程数、最大线程数、空闲时间、任务队列、线程工厂和拒绝策略。根据业务类型(CPU/IO密集型)合理配置参数,使用`ThreadPoolExecutor`构造函数创建,推荐结合监控和动态调整能力。
Process finished with exit code 0