当前位置: 首页 > news >正文

LangChain基础实践——论文阅读助手

本文代码由Coding Agent辅助编写,所使用Coding Agent包括Trae和Tencent Cloud Codebuddy,LLM为deepseek v4 pro。

0 介绍LangChain

LangChain面向LLM应用开发设计,它的贡献是将LLM模型与各种工具、数据源和业务逻辑连接起来,以提高LLM应用特别是Agent应用(主要通过LangGraph实现,是他们目前的重点方向)的开发效率。

本文我们基于LangChain开发一个论文阅读助手,本质是一个设定了特定system propmt的chatbot,主要关注LangChain提供的高效工具实现以及代码特点。

1 需求分析

1.1 流程图

1.2 技术栈

PDF解析:PyMuPDFLoader(langchain_community.document_loaders)

切chunk:RecursiveCharacterTextSplitter(langchain_text_splitters)

模型调用:ChatOpenAI、OpenAIEmbeddings(langchain_openai)

prompt:PromptTemplate(langchain_classic.prompts)

向量数据库:FAISS(langchain_community.vectorstores)

检索:RetrievalQA(langchain_classic.chains)

从这里也可以看出,langchain框架高度覆盖了LLM应用开发中的常用工具,并封装为了统一接口。

2 环境配置

conda create -n langchain python==3.10 -y conda activate langchain pip install -u langchain pip install -U langchain-openai pip install openai pypdf pymupdf langchain-community faiss-cpu sentence-transformers langchain_text_splitters

3 开发

3.1 Config类

@dataclass class PaperReaderConfig: """论文阅读Agent配置""" chunk_size: int = 300 chunk_overlap: int = 50 embedding_mode: str = "api" embedding_api_url: str = "https://api.siliconflow.cn/v1" embedding_model_name: str = "BAAI/bge-large-zh-v1.5" embedding_api_key: Optional[str] = field(default=None) local_model_name: str = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" local_model_path: str = "./embedding_model" search_k: int = 15 search_fetch_k: int = 20 mmr_lambda: float = 0.5 include_abstract: bool = True abstract_pages: int = 2 vector_store_path: str = "./faiss_index" llm_model_name: str = "deepseek-chat" llm_api_base: str = "https://api.deepseek.com" llm_api_key: Optional[str] = field(default=None) temperature: float = 0.1

分块配置:chunk_size和chunk_overlap表示文档切块的大小和每个切块间的重合数。此处,重合数的意义是使得切分时被切断的句子出现在前一chunk的末尾和后一chunk的开头,以期保持更好的语义完整性。

嵌入模型配置:embedding设计支持API调用和本地调用,本文使用API调用(其实是因为最早在WSL里下模型,懒得配代理导致连不上hf,就改成API调用了)。此处使用的是硅基流动提供的bge-large-zh-v1.5模型,是个免费API嵌入模型,只要申请API就能用。API key此处写成占位符,调试时可以硬编码在这里,使用时建议从环境变量读取,也可以从前端输入覆盖配置类。

检索配置:关于MMR的具体原理请参考论文《The use of MMR, diversity-based reranking for reordering documents and producing summaries》。这部分检索会根据相似度排序粗筛选search_fetch_k(20)个chunk,在其中用MMR算法(λ=0.5)最终筛选search_k(15)个chunk作为检索结果。include_abstract配置项用于向LLM上下文直接注入摘要,为模型提供全文的感知。

LLM配置:使用API模型,调用Deepseek V4。具体细节和嵌入模型一致。此处如果只是想实验,可以去​​​​​​​OpenRouter找一些免费模型,OpenRouter本身就支持OpenAI API,可以直接嵌入到本文流程中。

3.2 摘要注入

class AbstractRetriever(BaseRetriever): """包装基础检索器,自动注入论文摘要到检索结果中""" base_retriever: BaseRetriever abstract_text: str include_abstract: bool def _get_relevant_documents(self, query: str, *, run_manager=None) -> List[Document]: docs = self.base_retriever.invoke(query) if self.include_abstract and self.abstract_text: prefix = f"[论文摘要/引言]\n{self.abstract_text}\n\n" for doc in docs: doc.page_content = prefix + doc.page_content return docs

这段代码重写了_get_relevant_documents方法以注入摘要,通过:

base_retriever: BaseRetriever docs = self.base_retriever.invoke(query)

实际的检索工作仍由base_retriever完成,检索完成后,包装器向docs中注入摘要。

3.3 主模块

class PaperReaderAgent: """论文阅读Agent(纯问答版本)""" QA_PROMPT = PromptTemplate( template="""你是一位专业的论文阅读助手,请基于提供的论文内容回答用户的问题。 如果问题的答案不在提供的论文内容中,请直接说"根据当前论文内容无法回答这个问题",不要编造答案。 请保持回答的准确性和简洁性,必要时可以引用原文内容。 论文内容: {context} 用户问题: {question} 请给出你的回答:""", input_variables=["context", "question"], ) def __init__(self, config: Optional[PaperReaderConfig] = None): self.config = config or PaperReaderConfig() self.vector_store: Optional[FAISS] = None self.qa_chain: Optional[RetrievalQA] = None self.embeddings = None self.llm = None self.abstract_text: str = "" self._init_components() def _init_embedding_api(self): api_key = self.config.embedding_api_key or os.environ.get("SILICONFLOW_API_KEY") if not api_key: raise ValueError( "未配置嵌入模型API Key,请设置 config.embedding_api_key 或环境变量 SILICONFLOW_API_KEY" ) logger.info("使用API嵌入模型: %s", self.config.embedding_model_name) self.embeddings = OpenAIEmbeddings( model=self.config.embedding_model_name, openai_api_base=self.config.embedding_api_url, openai_api_key=api_key, tiktoken_enabled=False, check_embedding_ctx_length=False, ) def _init_embedding_local(self): model_path = os.path.abspath(self.config.local_model_path) if os.path.isdir(model_path) and os.listdir(model_path): logger.info("从本地路径加载嵌入模型: %s", model_path) else: if not os.environ.get("HF_ENDPOINT"): os.environ["HF_ENDPOINT"] = "https://hf-mirror.com" logger.info("从HuggingFace加载嵌入模型: %s", self.config.local_model_name) model_path = self.config.local_model_name self.embeddings = HuggingFaceEmbeddings( model_name=model_path, model_kwargs={"device": "cpu"}, encode_kwargs={"normalize_embeddings": True}, ) def _init_components(self): if self.config.embedding_mode == "api": self._init_embedding_api() else: self._init_embedding_local() llm_api_key = self.config.llm_api_key or os.environ.get("DEEPSEEK_API_KEY") if not llm_api_key: raise ValueError( "未配置LLM API Key,请设置 config.llm_api_key 或环境变量 DEEPSEEK_API_KEY" ) logger.info("初始化LLM: %s", self.config.llm_model_name) self.llm = ChatOpenAI( model_name=self.config.llm_model_name, temperature=self.config.temperature, openai_api_key=llm_api_key, openai_api_base=self.config.llm_api_base, ) def load_pdf(self, pdf_path: str) -> None: if not os.path.exists(pdf_path): raise FileNotFoundError(f"PDF文件不存在: {pdf_path}") logger.info("读取PDF: %s", pdf_path) loader = PyMuPDFLoader(pdf_path) documents = loader.load() documents = [doc for doc in documents if doc.page_content.strip()] total_chars = sum(len(doc.page_content) for doc in documents) logger.info("读取完成,共%d页,总字符数: %d", len(documents), total_chars) if self.config.include_abstract and documents: self.abstract_text = "\n\n".join( doc.page_content for doc in documents[: self.config.abstract_pages] ) logger.info("提取前%d页作为摘要 (%d字符)", self.config.abstract_pages, len(self.abstract_text)) text_splitter = RecursiveCharacterTextSplitter( chunk_size=self.config.chunk_size, chunk_overlap=self.config.chunk_overlap, separators=["\n\n", "\n", ". ", " ", ""], length_function=len, ) chunks = text_splitter.split_documents(documents) logger.info("分块完成,共%d个文本块", len(chunks)) logger.info("构建向量索引...") self.vector_store = FAISS.from_documents(chunks, self.embeddings) logger.info("向量索引构建完成") self._build_qa_chain() self.save_vector_store() def _build_qa_chain(self): if not self.vector_store: raise ValueError("请先加载PDF文档") base_retriever = self.vector_store.as_retriever( search_type="mmr", search_kwargs={ "k": self.config.search_k, "fetch_k": self.config.search_fetch_k, "lambda_mult": self.config.mmr_lambda, }, ) retriever = AbstractRetriever( base_retriever=base_retriever, abstract_text=self.abstract_text, include_abstract=self.config.include_abstract, ) self.qa_chain = RetrievalQA.from_chain_type( llm=self.llm, chain_type="stuff", retriever=retriever, chain_type_kwargs={"prompt": self.QA_PROMPT}, ) def ask(self, question: str) -> str: if not self.qa_chain: raise ValueError("请先加载PDF文档") logger.info("处理问题: %s", question) response = self.qa_chain.invoke({"query": question}) return response["result"] def save_vector_store(self): if self.vector_store: self.vector_store.save_local(self.config.vector_store_path) logger.info("向量索引已保存到: %s", self.config.vector_store_path) def load_vector_store(self): if not os.path.exists(self.config.vector_store_path): raise FileNotFoundError(f"向量索引路径不存在: {self.config.vector_store_path}") self.vector_store = FAISS.load_local( self.config.vector_store_path, self.embeddings, allow_dangerous_deserialization=True, ) self._build_qa_chain() logger.info("向量索引已从%s加载", self.config.vector_store_path)

下面逐方法介绍模块。

3.3.1 prompt模板

QA_PROMPT = PromptTemplate( template="""你是一位专业的论文阅读助手,请基于提供的论文内容回答用户的问题。 如果问题的答案不在提供的论文内容中,请直接说"根据当前论文内容无法回答这个问题",不要编造答案。 请保持回答的准确性和简洁性,必要时可以引用原文内容。 论文内容: {context} 用户问题: {question} 请给出你的回答:""", input_variables=["context", "question"], )

此处提供了prompt模板,PromptTemplate类由langchain_classic.prompts提供(旧版本中为langchain.prompts)。

PromptTemplate类使用str.format()语法,{}会自动解释为占位符,如果需要{}作为字符串存在,需要用{{和}}转义。这在输出为json、需要规定具体结构时可能有用。

另外,通过修改此处的system prompt,特别是角色,可以让模型作为其它领域的chatbot工作,比如pdf是简历、说明书等。也可以适当调整内容,将角色等内容设计为占位符,由用户自行设置。

最后,这个模板并没有针对prompt注入进行有效防护措施。因为本文开发的只是一个chatbot,在调用API模型时,prompt注入攻击所能产生的恶性后果可预见地非常有限。对于Agent,必须配置良好的防护措施,取决于你Agent的功能边界。

3.3.2 类初始化

def __init__(self, config: Optional[PaperReaderConfig] = None): self.config = config or PaperReaderConfig() self.vector_store: Optional[FAISS] = None self.qa_chain: Optional[RetrievalQA] = None self.embeddings = None self.llm = None self.abstract_text: str = "" self._init_components()

没有特别需要强调的细节。

3.3.3 加载嵌入模型

def _init_embedding_api(self): api_key = self.config.embedding_api_key or os.environ.get("SILICONFLOW_API_KEY") if not api_key: raise ValueError( "未配置嵌入模型API Key,请设置 config.embedding_api_key 或环境变量 SILICONFLOW_API_KEY" ) logger.info("使用API嵌入模型: %s", self.config.embedding_model_name) self.embeddings = OpenAIEmbeddings( model=self.config.embedding_model_name, openai_api_base=self.config.embedding_api_url, openai_api_key=api_key, tiktoken_enabled=False, check_embedding_ctx_length=False, )

self.config.embedding_api_key是预留给前端输入的。具体而言:

def _create_agent( embedding_key: str, llm_key: str, temperature: float, embedding_mode: str = "api", ) -> PaperReaderAgent: """根据 UI 参数创建 Agent 实例""" config = PaperReaderConfig( embedding_mode=embedding_mode, embedding_api_key=embedding_key or None, llm_api_key=llm_key or None, temperature=temperature, ) return PaperReaderAgent(config)
ui.label("Embedding 模型:BAAI/bge-large-zh-v1.5 · API:SiliconFlow").classes("text-xs text-gray-500") embedding_key_input = ( ui.input( label="SiliconFlow API Key", password=True, password_toggle_button=True, value=os.environ.get("SILICONFLOW_API_KEY", ""), ) .classes("w-full") .props("clearable") )

_init_embedding_local未实际使用,跳过。_init_components将_init_embedding_api和LLM一并初始化,逻辑与_init_embedding_api一致,跳过。

3.3.4 加载PDF

def load_pdf(self, pdf_path: str) -> None: if not os.path.exists(pdf_path): raise FileNotFoundError(f"PDF文件不存在: {pdf_path}") logger.info("读取PDF: %s", pdf_path) loader = PyMuPDFLoader(pdf_path) documents = loader.load() documents = [doc for doc in documents if doc.page_content.strip()] total_chars = sum(len(doc.page_content) for doc in documents) logger.info("读取完成,共%d页,总字符数: %d", len(documents), total_chars) if self.config.include_abstract and documents: self.abstract_text = "\n\n".join( doc.page_content for doc in documents[: self.config.abstract_pages] ) logger.info("提取前%d页作为摘要 (%d字符)", self.config.abstract_pages, len(self.abstract_text)) text_splitter = RecursiveCharacterTextSplitter( chunk_size=self.config.chunk_size, chunk_overlap=self.config.chunk_overlap, separators=["\n\n", "\n", ". ", " ", ""], length_function=len, ) chunks = text_splitter.split_documents(documents) logger.info("分块完成,共%d个文本块", len(chunks)) logger.info("构建向量索引...") self.vector_store = FAISS.from_documents(chunks, self.embeddings) logger.info("向量索引构建完成") self._build_qa_chain() self.save_vector_store()

使用配置中的abstract_pages对论文摘要进行提取,得到的字符串为self.abstract_text,以供后续步骤使用该属性作为注入内容。

text_splitter使用的是langchain_text_splitters提供的RecursiveCharacterTextSplitter,这是最常用的切分器之一。其主要特点是按换行、段落等结束符切分,优先保持语义完整。可通过separators参数自定义切分标识,如代码切分可“\nclass”。对于论文而言,用换行切分基本足够使用。

切分完成后,通过FAISS建库,第一个参数是chunks,第二个参数是self.embeddings = OpenAIEmbeddings(...),是用于将chunk转换为嵌入的模型。

建库完成获得向量库self.vector_store,进入下一步流程。

3.3.5 QA Chain

def _build_qa_chain(self): if not self.vector_store: raise ValueError("请先加载PDF文档") base_retriever = self.vector_store.as_retriever( search_type="mmr", search_kwargs={ "k": self.config.search_k, "fetch_k": self.config.search_fetch_k, "lambda_mult": self.config.mmr_lambda, }, ) retriever = AbstractRetriever( base_retriever=base_retriever, abstract_text=self.abstract_text, include_abstract=self.config.include_abstract, ) self.qa_chain = RetrievalQA.from_chain_type( llm=self.llm, chain_type="stuff", retriever=retriever, chain_type_kwargs={"prompt": self.QA_PROMPT}, )

as_retriever方法将self.vector_store包装为标准的Retriever对象,可以将检索器base_retriever理解为向量库self.vector_store的访问接口。

通过3.2中定义的AbstractRetriever,为检索器提供摘要注入的能力。

最后,构筑QA Chain,RetrievalQA是langchain中用于RAG的标准chain,作用是组装LLM、检索器和prompt。大致过程包括:接收用户输入-调用检索器-构建prompt-调用LLM-返回LLM输出。

3.3.6 面向用户的接口ask

def ask(self, question: str) -> str: if not self.qa_chain: raise ValueError("请先加载PDF文档") logger.info("处理问题: %s", question) response = self.qa_chain.invoke({"query": question}) return response["result"]

已经完成了QA Chain的构造,还需要一个转换接口完成对QA Chain的调用。这里ask方法进行了PDF文档检查和question格式转换的功能。

可以看出,self.qa_chain通过invoke方法运行,它本身的runnable规范更接近一个可调用的方法,早期的类也确实可以通过__call__调用(),但由于不易维护、不易扩展和可读性差等诸多不利因素被舍弃。详情参考:https://ai123.blog.csdn.net/article/details/147641943。

3.4 入口函数

def main(): """示例使用""" config = PaperReaderConfig() agent = PaperReaderAgent(config) agent.load_pdf("./test_paper.pdf") answer = agent.ask("文章的主要贡献是什么") print(f"回答: {answer}")

4 WebUI

前端是我用nicegui+codebuddy改出来的,但因为我不是很懂webui(原本是做pyqt的,但怕langchain和Windows兼容不好就放在WSL里做了,所以webui最方便,另外兼容性不好纯属我臆想的,实际上并没有此事),故只是放出这部分代码,不详细介绍细节了:

""" 基于 NiceGUI 的论文阅读助手 WebUI """ import json import os import tempfile from typing import Optional from nicegui import ui from paper_reader import PaperReaderAgent, PaperReaderConfig # ── 全局状态 ────────────────────────────────────────────────────── agent: Optional[PaperReaderAgent] = None loaded_pdf_name: str = "" # ── 辅助函数 ────────────────────────────────────────────────────── def _create_agent( embedding_key: str, llm_key: str, temperature: float, embedding_mode: str = "api", ) -> PaperReaderAgent: """根据 UI 参数创建 Agent 实例""" config = PaperReaderConfig( embedding_mode=embedding_mode, embedding_api_key=embedding_key or None, llm_api_key=llm_key or None, temperature=temperature, ) return PaperReaderAgent(config) # ── 主页面 ──────────────────────────────────────────────────────── @ui.page("/") def main_page(): global agent, loaded_pdf_name # ── 自定义样式 ── ui.add_head_html(""" <style> body { background: #f0f4f8; } .header-card { background: linear-gradient(135deg, #0D47A1 0%, #1976D2 100%); color: #ffffff; border-radius: 12px; padding: 24px 28px; margin-bottom: 20px; box-shadow: 0 4px 20px rgba(25,118,210,0.25); } .status-loaded { color: #2E7D32; font-weight: 600; } .status-empty { color: #9E9E9E; } .result-area { background: #fafbfc; border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; min-height: 200px; max-height: 600px; overflow-y: auto; white-space: pre-wrap; font-size: 14px; line-height: 1.7; } .result-area blockquote { border-left: 3px solid #1976D2; padding-left: 12px; margin: 8px 0; color: #555; } .section-label { font-size: 13px; color: #78909C; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; } /* 上传组件:将完成状态 ✓ 改为 X,仅作用于文件头部图标 */ .q-uploader__file--status-uploaded .q-uploader__file-header .q-icon { visibility: hidden; position: relative; } .q-uploader__file--status-uploaded .q-uploader__file-header .q-icon::after { visibility: visible; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); font-family: 'Material Icons', 'Material Icons Outlined', sans-serif; font-size: 24px; content: 'close'; } /* 保留文件类型图标(第一个 icon)不变 */ .q-uploader__file--status-uploaded .q-uploader__file-header .q-icon:first-child::after { content: none; } .q-uploader__file--status-uploaded .q-uploader__file-header .q-icon:first-child { visibility: visible; } </style> """) # ── 顶部标题栏 ── with ui.element("div").classes("header-card"): with ui.row().classes("items-center gap-4"): ui.icon("menu_book", size="36px").classes("opacity-90") with ui.column().classes("gap-0"): ui.label("论文阅读助手").classes("text-2xl font-bold tracking-wide") ui.label("LangChain + BGE Embedding + DeepSeek").classes("text-sm opacity-75") # ══════════════════════════════════════════════════════════════ # 第一行:PDF 上传 + API 配置 # ══════════════════════════════════════════════════════════════ with ui.row().classes("w-full gap-4"): # ── Card 1:PDF 文档 ── with ui.card().classes("flex-1 min-w-[320px]"): ui.label("📁 选择论文 PDF").classes("text-lg font-semibold text-blue-900") ui.separator() upload = ui.upload( label="拖拽或点击上传 PDF", on_upload=lambda e: handle_upload(e), auto_upload=True, ).classes("w-full").props("max-file-size=52428800 max-files=1") # 50MB, 仅1个文件 pdf_status = ui.label("未加载文档").classes("status-empty text-sm") load_index_checkbox = ui.checkbox("加载已有索引 (跳过PDF解析)", value=False).classes("mt-2") load_index_btn = ui.button( "加载本地索引", on_click=lambda: handle_load_index(), icon="folder_open", ).classes("mt-1").props("outline").bind_visibility_from(load_index_checkbox, "value") # ── Card 2:API 配置 ── with ui.card().classes("flex-1 min-w-[320px]"): ui.label("🔑 API 配置").classes("text-lg font-semibold text-blue-900") ui.separator() ui.label("Embedding 模型:BAAI/bge-large-zh-v1.5 · API:SiliconFlow").classes("text-xs text-gray-500") embedding_key_input = ( ui.input( label="SiliconFlow API Key", password=True, password_toggle_button=True, value=os.environ.get("SILICONFLOW_API_KEY", ""), ) .classes("w-full") .props("clearable") ) ui.label("LLM 模型:DeepSeek-Chat · API:api.deepseek.com").classes("text-xs text-gray-500 mt-3") llm_key_input = ( ui.input( label="DeepSeek API Key", password=True, password_toggle_button=True, value=os.environ.get("DEEPSEEK_API_KEY", ""), ) .classes("w-full") .props("clearable") ) with ui.row().classes("items-center gap-2 mt-2 w-full"): ui.label("温度").classes("text-sm text-gray-600 shrink-0") temp_slider = ( ui.slider(min=0.0, max=1.0, step=0.05, value=0.1) .classes("flex-1") .props('label-always') ) temp_label = ui.label("0.10").classes("text-sm font-mono w-10 text-right shrink-0") temp_slider.on( "update:model-value", lambda e: temp_label.set_text(f"{e.args:.2f}"), ) # ══════════════════════════════════════════════════════════════ # 第二行:提问 # ══════════════════════════════════════════════════════════════ with ui.card().classes("w-full mt-4"): ui.label("💬 提问").classes("text-lg font-semibold text-blue-900") ui.separator() question_input = ( ui.textarea( label="输入你的问题", placeholder="例如:这篇文章的主要贡献是什么?", ) .classes("w-full mt-3") .props("rows=3 outlined") ) with ui.row().classes("mt-3 gap-3"): submit_btn = ui.button( "提交", on_click=lambda: handle_submit(), icon="send", ).classes("bg-blue-700 text-white") clear_btn = ui.button( "清空输出", on_click=lambda: _set_result(""), icon="clear_all", ).props("flat") spinner = ui.spinner(size="sm").classes("mt-1") spinner.set_visibility(False) # ══════════════════════════════════════════════════════════════ # 第三行:结果展示 # ══════════════════════════════════════════════════════════════ with ui.card().classes("w-full mt-4"): with ui.row().classes("w-full items-center justify-between"): ui.label("📋 输出结果").classes("text-lg font-semibold text-blue-900") with ui.row().classes("gap-2"): copy_btn = ui.button( "复制", icon="content_copy", on_click=lambda: ui.run_javascript( f'navigator.clipboard.writeText({json.dumps(result_text["value"])})' ), ).props("flat dense") ui.separator() # 存储结果文本,供复制按钮使用 result_text = {"value": ""} result_area = ui.html().classes("result-area") def _set_result(content: str) -> None: """更新结果区域并同步复制文本""" result_text["value"] = content result_area.content = content # ══════════════════════════════════════════════════════════════ # 事件处理 # ══════════════════════════════════════════════════════════════ async def handle_upload(e): """处理 PDF 文件上传(NiceGUI 3.x: 文件数据在 e.file 中)""" global agent, loaded_pdf_name file_upload = e.file # NiceGUI 3.x FileUpload 对象 filename = file_upload.name embedding_key = embedding_key_input.value.strip() llm_key = llm_key_input.value.strip() if not embedding_key: ui.notify("请先填写 SiliconFlow API Key", type="warning", position="top") return if not llm_key: ui.notify("请先填写 DeepSeek API Key", type="warning", position="top") return try: spinner.set_visibility(True) pdf_status.set_text("⏳ 正在解析 PDF,构建向量索引...") ui.notify(f"开始处理: {filename}", type="info", position="top") # 异步保存到临时文件 fd, tmp_path = tempfile.mkstemp(suffix=".pdf") os.close(fd) await file_upload.save(tmp_path) agent = _create_agent( embedding_key=embedding_key, llm_key=llm_key, temperature=temp_slider.value, ) agent.load_pdf(tmp_path) loaded_pdf_name = filename pdf_status.set_text(f"✅ 已加载: {loaded_pdf_name}").classes( "status-loaded text-sm" ) ui.notify("PDF 加载完成!可以开始提问了", type="positive", position="top") try: os.unlink(tmp_path) except OSError: pass # 上传完成后清空队列,允许下次直接替换 upload.reset() except Exception as ex: pdf_status.set_text(f"❌ 加载失败: {ex}").classes("text-red-500 text-sm") ui.notify(f"加载失败: {ex}", type="negative", position="top") finally: spinner.set_visibility(False) def handle_load_index(): """从本地加载已有向量索引""" global agent, loaded_pdf_name embedding_key = embedding_key_input.value.strip() llm_key = llm_key_input.value.strip() if not embedding_key or not llm_key: ui.notify("请先填写 API Key", type="warning", position="top") return try: spinner.set_visibility(True) pdf_status.set_text("⏳ 正在加载本地索引...") agent = _create_agent( embedding_key=embedding_key, llm_key=llm_key, temperature=temp_slider.value, ) agent.load_vector_store() loaded_pdf_name = "(从本地索引加载)" pdf_status.set_text(f"✅ 已加载本地向量索引").classes( "status-loaded text-sm" ) ui.notify("向量索引加载完成", type="positive", position="top") except Exception as ex: pdf_status.set_text(f"❌ 加载失败: {ex}").classes("text-red-500 text-sm") ui.notify(f"加载失败: {ex}", type="negative", position="top") finally: spinner.set_visibility(False) def handle_submit(): """提问题""" global agent if not agent: ui.notify("请先加载 PDF 文档", type="warning", position="top") return question = question_input.value.strip() if not question: ui.notify("请输入问题", type="warning", position="top") return temperature = temp_slider.value # 如果温度变了,更新 LLM 温度 if agent.llm and agent.llm.temperature != temperature: agent.llm.temperature = temperature try: spinner.set_visibility(True) submit_btn.disable() answer = agent.ask(question) _set_result(answer) ui.notify("完成!", type="positive", position="top") except Exception as ex: _set_result( f'<div style="color:#c62828;padding:12px;">' f'<strong>错误:</strong> {ex}</div>' ) ui.notify(f"处理失败: {ex}", type="negative", position="top") finally: spinner.set_visibility(False) submit_btn.enable() # ── 启动入口 ─────────────────────────────────────────────────────── if __name__ in {"__main__", "__mp_main__"}: ui.run( title="论文阅读助手", favicon="📄", port=8080, reload=False, dark=False, )

UI效果:

5 展望

其实这个项目很简单,就是过了一下langchain的基本语法和方法。在这个项目基础上可改进如下:

  • 前端支持嵌入模型和LLM选择、支持参数配置、支持自定义system prompt
  • prompt防注入优化
  • 不用QA,而是用React,做成真正的agent,这里最直接的三个tool是:arxiv直接读取论文、论文内github链接检索解析(甚至部署)、本文注入摘要工具化由模型选择是否需要摘要

参考资料

LangChain:https://www.langchain.com/

【LangChain】BaseLLM.__call__ 方法(即直接调用 LLM 对象)迁移至 invoke 方法

http://www.gsyq.cn/news/1606079.html

相关文章:

  • 华大九天加大投资并购力度,韬定律驱动EDA全流程加速布局
  • 2026年企业采购AI外呼系统:怎么选性价比更高?
  • 淘宝电商课程哪个更适合新手
  • pg空值管理
  • 主流办公APP对比,图文会议总结功能谁更实用
  • 621万vs697万!2026年结婚人数预测你信哪个?
  • Python列表去重的20种实现方式
  • 时间管理:番茄工作法在编程中的应用
  • title: Claude Code 教程:从零搭建 AI 驱动的开发工作流(基于 Google 新版 SDLC 白皮书)
  • Linux:进程
  • SpringBoot 整合 MinIO 实现文件存储——私有化 OSS 方案
  • 吉阳区正宗椰子鸡推荐|符合海南本土特色的宝藏门店
  • 《AI抢产能致车规存储缺货?欣芯半导体给出eMMC/UFS“供应韧性”破局与选型指南》
  • 如何甄选靠谱展厅设计公司:从效果到落地的实战指南
  • 2026去水印不破坏原图的方法!电脑手机在线无痕去水印工具+PS教程
  • Java计算机毕设之基于 Java 的部门通知与任务一体化管理系统 团队协作型任务分配管理系统(完整前后端代码+说明文档+LW,调试定制等)
  • 2026奶茶店收银系统维护商推荐解析:凤梨收银系统适配茶饮业态的专业参考
  • 专业的杭州Geo哪家有实力
  • 查询优化-提升子查询-UNION类型
  • STM32和STM32CubeMX实现SHT30温湿度传感器 保姆级教程
  • 社区公益服务平台 Java+SpringBoot+Vue 前后分离
  • 营销智能体选哪个?一份基于实际场景的对比指南正在改变内容生成、投放优化和用户互动的效率。但市场上的产品形态差异很大:有的只是套了壳的通用写作工具,有的是传统营销 SaaS 加了个 AI 入口。选错不仅
  • 口碑佳的智能产品有何奥秘
  • 收藏!AI应用开发路线图:Java后端+Python大模型,小白也能轻松入门并快速上手
  • 软件数据可视化化的图表展示与交互
  • 暑假将至,校园安防不“放假”:国标GB28181视频监控平台EasyCVR这套视频融合方案让安全“全年无休”
  • [百度网盘] 大模型AI应用开发企业级项目实战(提示词工程+大模型NLP应用+AI对话产品)
  • 数据库巡检怎么做?Prometheus+Grafana监控体系搭建指南
  • 记一次由「系统Swap空间」被频繁使用导致的性能急剧下降
  • 软件检测实验室CMA资质认定技术人员和管理人员岗位要求与职责划分