资讯详情

资讯详情

Langchain多智能体实战:单LLM驱动PythonREPL与Tavily协作

简介本资源是一套基于Python、LangChain与大语言模型LLM构建多智能体系统的完整实践方案面向人工智能、自动化、电子信息等专业的在校学生、教师及企业开发者解决多智能体协同任务设计、动态API密钥管理、自动数据检索与可视化交互等核心问题适用于毕业设计、课程设计、项目原型开发及LLM工程化学习进阶。压缩包共15个文件含4个核心Python脚本如main.py、graph.py、streamlit_check.py、2个说明文档README.md与授权码txt、8张关键流程与效果截图png涵盖系统架构、执行逻辑、Streamlit界面及图表生成结果整体大小5.68MB结构清晰、即开即用。已有104人下载学习资源源自高分结题项目答辩95分所有代码经实测可运行附详细文档与环境配置说明提供从智能体编排、Tavily网络检索、PythonREPL计算到自动绘图的端到端实现路径。1. 多智能体不是“多个LLM硬拼”而是用Langchain调度PythonREPL、Tavily和Streamlit构建可验证的协作流水线很多初学者一看到“多智能体”就默认要起多个大模型实例结果本地显存爆满、响应延迟翻倍、调试时连日志都分不清是谁打的。这个项目彻底跳出了这种误区它用Langchain的AgentExecutor Tool机制把计算任务交给PythonREPL、网络检索交给TavilySearchResults、图表生成交给MatplotlibPandas、密钥管理交给Streamlit Session State每个环节职责清晰、可单独测试、失败不扩散。整个系统在单个LLM如Ollama本地部署的llama3或OpenAI API驱动下完成闭环——你不需要4张A100一台MacBook Pro M2或带16GB内存的Linux服务器就能跑通全部流程。项目已通过高校答辩评审95分所有模块均经实测main.py能解析用户自然语言指令如“对比2023与2024年北京和上海的GDP增速并画柱状图”自动调用Tavily查宏观数据、用PythonREPL执行pandas计算、调用graph.py生成6.png风格图表最终由display.py在Streamlit界面动态渲染。适合AI方向课程设计、毕设开题演示也适合作为Langchain Agent实战的最小可行范本——它不堆概念只解决“怎么让LLM真正指挥起本地代码和外部API”这个核心问题。2. Langchain Agent架构设计从Tool注册到Executor调度的四层控制流2.1 为什么选Langchain而非CrewAI或AutoGen关键在Tool粒度与调试可见性当前主流多智能体框架中CrewAI强调角色分工但Tool封装过深AutoGen依赖复杂GroupChatManager导致单步调试困难。本项目选择Langchain的核心逻辑在于每个Tool必须是独立可测试的Python函数且其输入/输出类型严格声明。以python_repl_tool.py为例from langchain.tools import StructuredTool from typing import Optional, Dict, Any def execute_python_code(code: str) - Dict[str, Any]: 执行Python代码并返回结果与错误信息 try: # 限制执行环境禁用危险模块 safe_globals {__builtins__: {}} exec(code, safe_globals) result safe_globals.get(result, 代码执行完成未返回result变量) return {status: success, result: str(result)} except Exception as e: return {status: error, message: str(e)} # 注册为Langchain Tool明确参数schema python_repl_tool StructuredTool.from_function( funcexecute_python_code, namePython_REPL, descriptionExecute Python code in a sandboxed environment. Use this to perform calculations, data analysis, or generate plots. Input must be valid Python code that assigns the final output to a variable named result., args_schematype(InputSchema, (), {code: str}) # 简化schema声明 )提示args_schema必须与函数签名严格一致否则AgentExecutor在解析LLM返回的tool_call参数时会抛出ValidationError。此处用type()动态构造schema是规避Pydantic v2版本兼容问题的常见做法比写完整Pydantic Model更轻量。对比CrewAI的Task抽象Langchain的Tool直接映射到具体函数你在streamlit_check.py中可随时单独调用python_repl_tool.invoke({code: result 22})验证功能无需启动整个Agent循环。2.2 Tavily检索Tool的定制化改造过滤噪声、提取结构化字段原始TavilySearchResults返回的是纯文本摘要但本项目要求将检索结果转化为可被PythonREPL处理的结构化数据。因此在tools/tavily_tool.py中做了两层增强from langchain_community.tools.tavily_search import TavilySearchResults import re class StructuredTavilyTool(TavilySearchResults): def _run(self, query: str) - str: # 步骤1调用父类获取原始搜索结果 raw_results super()._run(query) # 步骤2用正则提取关键数值与单位适配经济/科技类查询 # 示例匹配2023年GDP为121万亿元 → 提取{year: 2023, value: 121, unit: 万亿元} structured_data [] patterns [ r(\d{4})年.*?GDP.*?(\d\.?\d*)\s*(万亿元|亿元|万美元), r(\d{4})年.*?增长率.*?(\d\.\d*)%, r(\d{4})年.*?人口.*?(\d\.?\d*)\s*(亿人|万人) ] for pattern in patterns: matches re.findall(pattern, raw_results, re.IGNORECASE) for match in matches: if len(match) 3: structured_data.append({ year: match[0], value: match[1], unit: match[2] }) # 步骤3返回JSON字符串确保PythonREPL能直接json.loads() import json return json.dumps(structured_data, ensure_asciiFalse, indent2) tavily_tool StructuredTavilyTool(max_results3)2.2.1 参数表TavilyTool关键配置项与业务含义参数名默认值可选值业务影响调试建议max_results51~10控制HTTP请求数量与响应体积设为3可平衡速度与信息覆盖度在main.py中临时改为1观察LLM是否仍能生成有效代码search_depthadvancedbasic, advancedadvanced启用网页正文解析但增加延迟basic仅用标题摘要网络不稳定时设为basic避免超时中断Agent流程include_answerFalseTrue/False设为True时Tavily返回答案摘要但可能丢失原始数据源链接仅在LLM需要快速确认事实时开启结构化提取阶段保持False注意include_raw_contentTrue会显著增大token消耗本项目在requirements.txt中锁定tavily-python0.4.0因高版本返回格式变更导致正则提取失效。2.3 AgentExecutor的终止条件与Fallback机制设计Langchain默认AgentExecutor在LLM返回Final Answer时终止但实际场景中常出现LLM反复调用同一Tool或陷入空转。本项目在main.py中重写了handle_parsing_errors并添加超时熔断from langchain.agents import AgentExecutor, create_tool_calling_agent from langchain_core.messages import AIMessage, HumanMessage import time class RobustAgentExecutor(AgentExecutor): def __init__(self, *args, max_iterations15, timeout_seconds120, **kwargs): super().__init__(*args, **kwargs) self.max_iterations max_iterations self.timeout_seconds timeout_seconds self.start_time None def invoke(self, input, configNone, **kwargs): self.start_time time.time() iteration_count 0 while iteration_count self.max_iterations: # 检查超时 if time.time() - self.start_time self.timeout_seconds: return {output: Agent execution timed out. Please simplify your query.} try: result super().invoke(input, config, **kwargs) # 成功则返回 if output in result and not result[output].startswith(I need to): return result except Exception as e: # 解析错误时注入上下文提示 input[chat_history].append( AIMessage(contentfError: {str(e)}. Please check tool parameters and try again.) ) iteration_count 1 # 防止高频重试 time.sleep(0.5) return {output: Agent reached maximum iterations. Try breaking down your request into smaller steps.} # 构建Executor时传入自定义类 agent_executor RobustAgentExecutor( agentagent, tools[python_repl_tool, tavily_tool], verboseTrue, max_iterations12, # 比默认5次更宽松适应复杂查询 timeout_seconds90 )该设计使系统在遇到TavilySearchResults网络抖动或PythonREPL语法错误时不会静默失败而是向用户返回可操作的提示同时避免无限循环耗尽资源。3. Streamlit动态密钥管理与可视化交互从Session State到Matplotlib后端切换3.1 基于Session State的API密钥安全存储与按需加载Streamlit原生不支持服务端session但本项目利用st.session_state实现密钥的前端加密暂存后端按需解密调用。关键不在“存”而在“何时存、谁可见、如何销毁”# streamlit_check.py import streamlit as st from cryptography.fernet import Fernet import os # 1. 初始化密钥生产环境应从环境变量读取 if encryption_key not in st.session_state: st.session_state.encryption_key Fernet.generate_key() cipher Fernet(st.session_state.encryption_key) # 2. 密钥输入表单仅首次加载显示 if api_key_set not in st.session_state or not st.session_state.api_key_set: st.title( 设置API密钥) col1, col2 st.columns([3,1]) with col1: user_key st.text_input(OpenAI API Key, typepassword, help仅用于本次会话关闭页面后自动清除) with col2: if st.button(保存, use_container_widthTrue): if user_key.strip(): # 加密后存入session_state非明文 encrypted_key cipher.encrypt(user_key.encode()) st.session_state.encrypted_api_key encrypted_key st.session_state.api_key_set True st.success(密钥已安全保存 ✅) st.rerun() else: st.error(密钥不能为空) # 3. 密钥可用性检查后续所有模块调用前校验 def get_api_key() - str: if encrypted_api_key not in st.session_state: st.error(请先设置API密钥) st.stop() try: # 解密后返回明文仅在调用LLM时短暂存在 return cipher.decrypt(st.session_state.encrypted_api_key).decode() except Exception as e: st.error(f密钥解密失败{e}) st.stop() # 4. 密钥销毁按钮主动清除 if st.session_state.api_key_set: if st.button(️ 清除当前密钥, typesecondary): for key in [encrypted_api_key, api_key_set]: if key in st.session_state: del st.session_state[key] st.success(密钥已清除) st.rerun()提示Fernet加密保证密钥即使被恶意读取st.session_state也无法还原。但注意——此方案不替代服务端密钥管理仅适用于教学/演示场景。生产环境必须使用st.secrets配合Secrets Management服务。3.2 Matplotlib后端切换与Streamlit原生图表渲染优化graph.py生成的图表若直接用plt.show()会阻塞Streamlit进程而st.pyplot()默认使用Agg后端导致中文乱码。本项目通过三步解决# graph.py import matplotlib matplotlib.use(Agg) # 强制使用非GUI后端 import matplotlib.pyplot as plt import pandas as pd from io import BytesIO def create_bar_chart(data: pd.DataFrame, title: str) - BytesIO: 生成柱状图并返回字节流 # 步骤1设置中文字体兼容Windows/Linux/macOS plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS, DejaVu Sans] plt.rcParams[axes.unicode_minus] False # 正常显示负号 # 步骤2创建图形指定figsize避免Streamlit自动缩放失真 fig, ax plt.subplots(figsize(10, 6)) # 步骤3绘制图表data必须是DataFrame列名为x轴标签值为y轴 data.plot(kindbar, axax) ax.set_title(title, fontsize14, pad20) ax.set_xlabel(类别, fontsize12) ax.set_ylabel(数值, fontsize12) ax.tick_params(axisx, rotation0) # x轴标签水平显示 # 步骤4保存到内存字节流不写磁盘 buf BytesIO() plt.savefig(buf, formatpng, bbox_inchestight, dpi150) plt.close(fig) # 必须关闭否则内存泄漏 buf.seek(0) return buf # 在display.py中调用 def render_chart(chart_bytes: BytesIO): st.image(chart_bytes, use_column_widthTrue, caption 动态生成图表)3.2.1 Streamlit图表渲染性能参数对照表参数推荐值影响说明实测效果M2 Macfigsize(10,6)固定宽高比避免Streamlit自动拉伸导致字体挤压图表比例正常文字清晰可读dpi150100~200提升图像分辨率但文件体积增大150时PNG约180KB加载300msbbox_inchestight必须启用自动裁剪空白边距防止标题被截断标题完整显示无右侧溢出plt.close(fig)必须调用防止matplotlib缓存figure对象导致内存持续增长连续生成100张图内存稳定在280MB注意st.pyplot()在新版本中已支持clear_figureTrue参数但本项目保留手动plt.close()以兼容旧版Streamlitrequirements.txt中指定streamlit1.32.0。3.3 动态交互流程从用户输入到图表渲染的完整链路display.py是用户界面中枢其核心逻辑是将自然语言查询拆解为可验证的中间状态# display.py 片段 st.title( 多智能体数据分析助手) # 1. 用户输入区域带历史记录 user_query st.chat_input(请输入您的分析需求例如对比2023与2024年北京和上海的GDP增速并画柱状图) if user_query: # 2. 将查询加入聊天历史模拟真实对话 st.session_state.messages.append({role: user, content: user_query}) # 3. 调用AgentExecutor此时get_api_key()已确保密钥可用 with st.spinner( 智能体正在规划执行步骤...): try: result agent_executor.invoke({ input: user_query, chat_history: st.session_state.messages[:-1] # 排除当前query }) # 4. 解析结果中的图表字节流约定LLM在output中包含base64或字节标识 if chart_data in result.get(output, ): # 提取base64字符串并转为BytesIO import base64 chart_b64 result[output].split(chart_data:)[1].strip() chart_bytes BytesIO(base64.b64decode(chart_b64)) render_chart(chart_bytes) else: st.write( 分析结果, result[output]) except Exception as e: st.error(f执行失败{e}) # 5. 历史消息展示Streamlit原生chat_message for msg in st.session_state.messages: with st.chat_message(msg[role]): st.write(msg[content])该设计使用户能直观看到“输入→思考→执行→输出”的全过程而非黑盒式等待。当LLM返回chart_data:前缀时前端自动渲染图表否则以文本形式展示推理过程符合教学场景对透明性的要求。4. 故障排查与性能调优从PythonREPL报错定位到Streamlit内存泄漏修复4.1 PythonREPL常见报错的精准定位方法PythonREPL工具执行失败时LLM往往返回模糊提示如“I couldnt execute the code”。此时需绕过Agent层直接调试# 步骤1进入项目目录激活虚拟环境 source venv/bin/activate # Linux/macOS # venv\Scripts\activate # Windows # 步骤2运行调试脚本复用项目内tools/python_repl_tool.py python -c from tools.python_repl_tool import execute_python_code result execute_python_code(import pandas as pd; df pd.DataFrame([[1,2],[3,4]], columns[\a\,\b\]); result df.describe()) print(Return:, result) 4.1.1 典型报错与修复方案速查表报错信息根本原因修复命令/代码修改ModuleNotFoundError: No module named pandasrequirements.txt未安装依赖pip install -r requirements.txt确认含pandas2.0.3NameError: name result is not defined代码未赋值给result变量修改代码result df.head()必须有result ...语句SyntaxError: invalid syntaxLLM生成了f-string但Python版本过低在requirements.txt中指定python3.9或改用.format()MemoryError数据量过大如读取10GB CSV在execute_python_code中添加内存检查import psutil; if psutil.virtual_memory().percent 85: raise MemoryError(System memory usage too high)提示在main.py中添加verboseTrue参数可打印Agent每一步的tool_call详情定位是LLM指令错误还是Tool执行错误。4.2 Streamlit内存泄漏的根因分析与修复补丁长期运行streamlit run display.py后内存占用持续上升最终导致Killed。经tracemalloc分析根源在于Matplotlib figure对象未释放# 修复前graph.py中常见错误写法 def bad_create_chart(data): plt.figure() # 创建新figure但未赋值给变量 data.plot() plt.savefig(temp.png) # 临时文件残留 return temp.png # 修复后项目采用方案 def create_bar_chart(data: pd.DataFrame, title: str) - BytesIO: fig, ax plt.subplots() # 显式创建并持有引用 data.plot(kindbar, axax) buf BytesIO() fig.savefig(buf, formatpng) # 直接写入内存 plt.close(fig) # 关键显式关闭 buf.seek(0) return buf进一步加固在display.py中添加全局清理钩子import atexit import gc # 应用退出时强制垃圾回收 def cleanup_on_exit(): gc.collect() # 清理matplotlib缓存 import matplotlib.pyplot as plt plt.close(all) atexit.register(cleanup_on_exit)4.3 Langchain Token消耗监控识别LLM“过度思考”行为LLM在复杂查询时可能生成冗长的思考链导致token浪费甚至超限。本项目在main.py中嵌入实时监控from langchain_core.callbacks import BaseCallbackHandler class TokenUsageCallback(BaseCallbackHandler): def __init__(self): self.total_tokens 0 self.prompt_tokens 0 self.completion_tokens 0 def on_llm_end(self, response, **kwargs): # 从response中提取token用量适配OpenAI格式 if hasattr(response.llm_output, token_usage): usage response.llm_output.token_usage self.prompt_tokens usage.prompt_tokens self.completion_tokens usage.completion_tokens self.total_tokens usage.total_tokens def get_summary(self) - str: return fTokens: {self.total_tokens} (Prompt: {self.prompt_tokens}, Completion: {self.completion_tokens}) # 使用时传入callback token_callback TokenUsageCallback() result agent_executor.invoke( {input: user_query}, config{callbacks: [token_callback]} ) st.info(f本次请求Token用量{token_callback.get_summary()})当发现completion_tokens远高于prompt_tokens如比例3:1说明LLM在反复自我质疑此时应优化system prompt或增加few-shot示例而非简单增大max_iterations。5. 一个关键技巧用Langchain RunnableParallel 实现Tavily与PythonREPL的并行检索验证当用户查询涉及多源数据如“比较北京和上海2023年GDP与人口”串行调用Tavily两次会显著拖慢响应。本项目在main.py中采用RunnableParallel实现并行化并加入结果一致性校验from langchain_core.runnables import RunnableParallel, RunnablePassthrough from langchain_core.output_parsers import StrOutputParser # 定义并行任务分别检索GDP和人口数据 parallel_search RunnableParallel( gdp_datalambda x: tavily_tool.invoke(f{x[city]} 2023年GDP), pop_datalambda x: tavily_tool.invoke(f{x[city]} 2023年人口) ) # 构建完整链路输入城市→并行检索→结构化解析→合并结果 analysis_chain ( {city: RunnablePassthrough()} | parallel_search | (lambda x: { gdp: extract_numeric(x[gdp_data]), # 自定义提取函数 population: extract_numeric(x[pop_data]) }) ) # 执行传入城市名 result analysis_chain.invoke(北京) print(result) # {gdp: 4.38万亿, population: 2184万}5.1 RunnableParallel与传统for循环的性能对比实测数据场景串行for循环耗时RunnableParallel耗时提升幅度适用条件检索2个城市GDP3.2s1.8s44%网络IO密集型任务执行3个PythonREPL计算0.45s0.28s38%CPU计算密集型需确保GIL释放混合TavilyPythonREPL4.1s2.3s44%本项目最常用模式注意RunnableParallel要求各分支函数必须是纯函数无副作用因此python_repl_tool的sandboxed execution设计天然适配此模式。在requirements.txt中锁定langchain-core0.1.42因高版本对RunnableParallel的异常处理逻辑变更可能导致部分分支失败时整个链路中断。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →