LangChain实战:构建具备RAG与计算能力的AI Agent
1. 从零构建AI Agent:LangChain实战指南
最近在开发一个需要结合知识检索和数学计算的智能助手项目时,我深入研究了LangChain框架的Agent机制。与普通聊天机器人不同,Agent能够自主调用工具、进行多轮决策,真正实现"会思考"的AI系统。下面分享我的实战经验,手把手教你构建一个具备RAG知识库查询和精确计算能力的智能Agent。
2. AI Agent核心架构解析
2.1 Agent的四大核心组件
一个完整的AI Agent需要具备以下能力:
- 大脑(LLM):使用通义千问(qwen-plus)作为推理引擎
- 记忆系统:
- 短期记忆:维护对话历史(message数组)
- 长期记忆:基于FAISS构建的RAG知识库
- 规划能力:通过多轮工具调用实现复杂任务分解
- 工具集:本例包含两个关键工具:
- 公司知识检索(rag_search)
- 数学计算器(calculator)
2.2 工具调用机制详解
LangChain的工具调用流程遵循以下范式:
- 用户输入查询(HumanMessage)
- LLM判断是否需要调用工具
- 若需调用,返回工具名称和参数(tool_calls)
- 执行具体工具函数
- 将结果封装为ToolMessage返回对话历史
- LLM基于工具结果生成最终回复
这个过程中最精妙的是第5步——通过ToolMessage将工具执行结果重新注入对话流,使得LLM能基于工具输出进行后续推理。
3. 实战开发步骤
3.1 环境准备与依赖安装
首先确保已安装必要库:
pip install langchain-core langchain-community faiss-cpu dashscope注意:FAISS在不同平台可能有兼容性问题,Mac用户建议使用conda安装:
conda install -c conda-forge faiss-cpu
3.2 工具函数实现
3.2.1 知识检索工具
@tool def rag_search(query: str) -> str: """ 从RAG知识库检索公司内部信息,包括: - 项目计划(名称/代号) - 技术方案 - 预算信息 - 截止日期 示例查询: - "深蓝计划的预算是多少" - "项目截止日期是什么时候" """ # 初始化向量数据库(具体实现见下文) ...关键点:
- 使用
@tool装饰器声明工具函数 - 文档字符串必须详细说明功能、参数和返回示例
- 返回类型必须为字符串
3.2.2 数学计算工具
@tool def calculator(expression: str) -> str: """ 执行精确数学计算,支持: - 四则运算(+ - * /) - 百分比计算 - 括号优先级 示例: - "2 + 3 * 5" → "17.0" - "500 * 0.8" → "400.0" """ # 安全计算实现见3.4节 ...3.3 多轮对话引擎实现
核心循环逻辑:
def run_agent(query: str, max_turns=5): tool_maps = {"rag_search": rag_search, "calculator": calculator} llm = ChatTongyi(model_name="qwen-plus") tool_llm = llm.bind_tools(tools=list(tool_maps.values())) messages = [HumanMessage(content=query)] for turn in range(max_turns): # 获取LLM响应 response = tool_llm.invoke(messages) messages.append(response) if not response.tool_calls: return response.content # 处理工具调用 for tool_call in response.tool_calls: tool_name = tool_call["name"] if tool_name in tool_maps: tool_output = tool_maps[tool_name](**tool_call["args"]) messages.append( ToolMessage( content=tool_output, tool_call_id=tool_call["id"], name=tool_name ) ) return "超过最大对话轮数"关键设计:通过max_turns参数防止无限循环,实测表明复杂任务通常3轮内可完成。
4. 安全增强方案
4.1 eval函数的安全隐患
原始计算器实现直接使用eval()存在严重风险:
# 危险实现! return str(eval(expression)) # 可能执行恶意代码4.2 三种加固方案
方案1:输入过滤
import re def safe_eval(expr: str) -> str: if not re.match(r'^[\d+\-*/(). ]+$', expr): return "非法输入" return str(eval(expr))方案2:使用ast.literal_eval
import ast def safe_eval(expr: str) -> str: try: node = ast.parse(expr, mode='eval') if not all(isinstance(n, (ast.Num, ast.BinOp, ast.UnaryOp)) for n in ast.walk(node)): raise ValueError return str(eval(expr)) except: return "计算错误"方案3:自定义解析器(推荐)
from operator import add, sub, mul, truediv ops = {'+': add, '-': sub, '*': mul, '/': truediv} def safe_calculate(expr: str) -> str: try: tokens = expr.split() stack = [] for token in tokens: if token in ops: b, a = stack.pop(), stack.pop() stack.append(ops[token](a, b)) else: stack.append(float(token)) return str(stack[0]) except: return "计算错误"实测表明方案3安全性最佳,虽然仅支持二元运算,但已满足大多数计算场景。
5. 性能优化技巧
5.1 向量数据库缓存
避免每次调用都重建FAISS索引:
RAG_PATH = "faiss_index" if os.path.exists(RAG_PATH): ragdb = FAISS.load_local(RAG_PATH, embeddings) else: ragdb = FAISS.from_documents(docs, embeddings) ragdb.save_local(RAG_PATH)5.2 对话历史管理
过长的对话历史会影响性能,建议:
- 设置最大历史长度(如最近10条)
- 对历史消息进行摘要压缩
- 重要信息显式存入知识库
5.3 工具调用优化
通过bind_tools的tool_choice参数可以控制工具调用行为:
# 强制使用特定工具 llm.bind_tools(..., tool_choice={"type": "function", "function": {"name": "calculator"}}) # 禁止工具调用 llm.bind_tools(..., tool_choice="none")6. 典型问题排查
6.1 工具未被识别
现象:LLM不调用预期工具 排查步骤:
- 检查工具描述是否完整(参数、示例缺一不可)
- 验证工具是否正确绑定:
print(tool_llm.tools) # 应显示已绑定的工具 - 测试直接工具调用是否正常
6.2 无限循环问题
现象:Agent持续调用同一工具 解决方案:
- 检查ToolMessage是否正确返回
- 限制最大对话轮数(如前文max_turns)
- 在工具描述中明确使用边界条件
6.3 中文处理异常
现象:中文查询结果不准确 优化方案:
- 调整文本分块策略:
text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, # 增大分块大小 chunk_overlap=100, separators=["\n\n", "\n", "。", "!", "?"] # 中文友好分隔符 ) - 使用支持中文的embedding模型:
embeddings = DashScopeEmbeddings(model="text-embedding-v2")
7. 扩展应用场景
基于此框架可轻松扩展更多实用场景:
7.1 电商客服Agent
- 新增工具:
- 订单查询
- 物流跟踪
- 退换货政策检索
7.2 数据分析Agent
- 集成:
- SQL查询
- 可视化生成
- 统计计算
7.3 智能家居控制
- 对接:
- 设备状态查询
- 情景模式切换
- 能耗统计
我在实际项目中发现,当工具数量超过5个时,建议使用ToolExecutor进行统一调度管理,避免工具冲突。另外,为每个工具添加明确的元数据描述(如适用场景、输入输出示例)能显著提升调用准确率。
