1. 从零开始构建AI Agent:LangChain实战指南
最近在开发一个需要结合大语言模型(LLM)和外部工具的项目时,我深入研究了LangChain框架。作为一个能够感知环境、进行推理并自主决策的智能系统,AI Agent远比普通聊天机器人强大得多。它不仅能理解自然语言,还能调用各种工具完成复杂任务。下面分享我在构建AI Agent过程中的实战经验和踩过的坑。
2. AI Agent核心架构解析
2.1 Agent四大核心组件
一个完整的AI Agent系统由四个关键部分组成:
-
LLM(大语言模型):负责自然语言理解和生成,是Agent的"大脑"。在项目中我使用的是通义千问(qwen-plus)模型,它对中文支持很好且API稳定。
-
记忆系统:
- 短期记忆:保存当前对话历史,让Agent能理解上下文
- 长期记忆:通过RAG(检索增强生成)技术接入知识库,我使用FAISS向量数据库存储公司内部文档
-
规划能力:Agent需要能拆解复杂任务为多个步骤,并决定何时调用哪个工具。LangChain提供了优秀的任务编排机制。
-
工具使用:Agent可以调用外部函数完成特定任务。在我的案例中实现了两个工具:精确计算器和文档检索系统。
提示:工具函数必须返回字符串类型,这是LangChain的强制要求。我在初期开发时曾因返回数字类型导致报错,调试了很久才发现这个问题。
2.2 工具函数设计规范
每个工具函数都需要遵循特定格式:
python复制@tool
def tool_name(parameters) -> str:
"""
函数描述(LLM通过这部分理解工具用途)
参数说明和示例
返回说明和示例
"""
# 工具实现
return "结果字符串"
在我的项目中,计算器工具是这样实现的:
python复制@tool
def calculator(expression: str) -> str:
"""
计算数学表达式。需要精确计算时使用。
参数:
expression: 数学算式,如 "2 + 2" 或 "500 * 0.8"。
返回:
str: 计算结果,如 "4.0" 或 "400.0"。
"""
print(f" [工具调用] 计算器正在计算: {expression}")
try:
return str(eval(expression))
except Exception as e:
return f"计算错误: {e}"
文档检索工具则更复杂一些,需要处理文本分块和向量存储:
python复制@tool
def rag_search(query: str) -> str:
"""
从数据库中搜索公司内部文档,包括项目计划、代号、截止日期等。
参数:
query: 搜索关键词,如"项目预算"或"截止日期"。
返回:
str: 相关文档内容。
"""
# 初始化FAISS向量数据库
if not os.path.exists(RAG_PATH):
# 文本分块处理
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=25,
chunk_overlap=5
)
split_docs = text_splitter.split_documents(docs)
ragdb = FAISS.from_documents(split_docs, embeddings)
ragdb.save_local(RAG_PATH)
else:
ragdb = FAISS.load_local(RAG_PATH, embeddings,
allow_dangerous_deserialization=True)
return "\n\n".join(doc.page_content for doc in
ragdb.similarity_search(query, k=2))
3. LangChain多轮对话实现
3.1 工具绑定与调用流程
让LLM能够调用工具的关键步骤是使用bind_tools方法:
python复制tool_maps = {
"rag_search": rag_search,
"calculator": calculator
}
llm = ChatTongyi(model_name="qwen-plus")
tool_llm = llm.bind_tools(tools=list(tool_maps.values()))
绑定后,LLM的响应会包含tool_calls字段,指示需要调用哪些工具。完整的工具调用流程如下:
- 用户输入问题
- LLM分析后决定调用工具
- 执行工具函数
- 将工具结果封装为ToolMessage
- 将ToolMessage加入对话历史
- LLM基于工具结果生成最终回复
3.2 多轮对话控制代码
python复制def run_agent(query: str):
message = [HumanMessage(content=query)]
# 限制最多5轮对话防止死循环
for i in range(5):
print(f"\n{'='*20}\n第{i+1}轮\n{query}\n{'='*20}")
response = tool_llm.invoke(message)
message.append(response)
if not response.tool_calls:
print("最终结果:" + response.content)
return
for tool_call in response.tool_calls:
func_name = tool_call["name"]
func_args = tool_call["args"]
if func_name in tool_maps:
tool_output = tool_maps[func_name].invoke(func_args)
print(f"工具调用:{func_name},参数:{func_args},结果:{tool_output}")
message.append(
ToolMessage(
content=tool_output,
tool_call_id=tool_call["id"],
name=func_name
)
)
else:
print(f"错误: 工具 {func_name} 不存在")
4. 实战案例演示
4.1 查询公司计划
python复制run_agent("公司计划是什么")
输出示例:
code复制====================
第1轮
公司计划是什么
====================
需要调用1个方法
工具调用:rag_search,参数:{'query': '公司计划'},结果:
【公司内部机密:代号"深蓝计划"】
1. 项目目标:开发一款能听懂猫语的翻译器。
2. 核心技术:基于Transformer的"喵声波"分析算法。
最终结果:公司当前正在推进"深蓝计划",目标是开发猫语翻译器...
4.2 复杂计算场景
python复制run_agent("公司的经费预算是多少,如果预算提高46%后多少")
这个查询会触发两次工具调用:
- 先调用rag_search获取原始预算(50元)
- 再调用calculator计算增加46%后的金额(50*1.46)
4.3 纯聊天场景
python复制run_agent("今天天气真好")
这种不需要工具调用的对话会直接由LLM响应,不会进入工具调用循环。
5. 安全风险与防范措施
5.1 eval函数的安全隐患
项目中计算器工具使用了Python的eval函数,这存在严重安全风险。恶意用户可能通过精心构造的输入执行任意代码,例如:
python复制"__import__('os').system('rm -rf /')"
5.2 安全加固方案
我最终采用了三种防护措施:
- 输入过滤:使用正则表达式只允许数字和基本运算符
python复制import re
def safe_calculator(expr: str) -> str:
if not re.match(r'^[\d\s+\-*/().]*$', expr):
return "错误: 包含非法字符"
# 剩余逻辑...
- 使用ast.literal_eval替代eval:它只能评估字面量表达式,更安全
python复制from ast import literal_eval
try:
result = literal_eval(expr)
except:
return "计算错误"
- LLM层面过滤:在系统提示词中加入指令,要求模型拒绝可疑计算请求
6. 性能优化与调试技巧
6.1 对话轮次控制
务必设置最大对话轮次限制(我设为5轮),否则某些情况下LLM会不断要求调用工具,形成死循环。在实际运行中,大部分任务在3轮内就能完成。
6.2 工具调用日志
完善的日志记录对调试至关重要。我为每个工具调用记录了:
- 调用时间
- 参数值
- 执行耗时
- 返回结果
python复制import time
def logged_tool(func):
def wrapper(*args, **kwargs):
start = time.time()
try:
result = func(*args, **kwargs)
status = "成功"
except Exception as e:
result = f"错误: {str(e)}"
status = "失败"
duration = time.time() - start
print(f"[{status}] {func.__name__} 耗时{duration:.2f}s | 输入: {args} | 输出: {result}")
return result
return wrapper
@tool
@logged_tool
def calculator(expr: str) -> str:
# 原有实现
6.3 向量数据库优化
对于RAG系统,我做了以下优化:
- 分块大小从固定25调整为动态范围(50-200)
- 添加了元数据过滤功能
- 实现了缓存机制减少重复检索
7. 扩展应用场景
基于这个基础框架,可以扩展出许多实用场景:
- 客户服务Agent:接入产品数据库和订单系统,自动回答客户咨询
- 数据分析Agent:结合SQL工具和可视化库,用自然语言查询数据
- 自动化办公Agent:集成邮件、日历和文档工具,辅助日常工作
我在项目中尝试接入了一个邮件发送工具,效果很不错:
python复制@tool
def send_email(to: str, subject: str, body: str) -> str:
"""
发送电子邮件
参数:
to: 收件人邮箱
subject: 邮件主题
body: 邮件正文
返回:
str: 发送结果
"""
# 实际实现会调用SMTP服务
return f"邮件已发送至{to}"
要让Agent学会合理使用这个工具,关键是在函数描述中提供清晰的用例说明。经过几次调试后,Agent已经能正确处理类似"给张三发邮件提醒他明天开会"这样的请求了。
8. 常见问题与解决方案
8.1 工具不被识别
问题:明明定义了工具,但LLM从不调用
检查点:
- 确保使用了@tool装饰器
- 函数描述文档要详细准确
- 确认bind_tools时传入了正确的工具列表
8.2 工具调用死循环
问题:Agent不断要求调用同一个工具
解决方案:
- 限制最大对话轮次
- 检查工具返回值是否符合预期
- 在系统提示词中明确限制重复行为
8.3 中文处理异常
问题:中文查询返回乱码或无关结果
调试方法:
- 确认LLM本身支持中文
- 检查文本编码设置
- 对中文文档调整分块策略(我最终使用按句子分割)
9. 部署实践心得
将开发好的Agent部署到生产环境时,我总结了以下经验:
- API密钥管理:千万不要硬编码在代码中!使用环境变量或专业密钥管理服务
- 错误处理:为每个工具添加超时控制和重试机制
- 性能监控:记录每个请求的响应时间和资源消耗
- 限流措施:防止API被滥用,我使用了令牌桶算法控制调用频率
对于需要处理敏感数据的场景,还应该:
- 实现数据脱敏
- 添加访问权限控制
- 开启详细的操作审计日志
10. 未来改进方向
虽然当前实现已经能满足基本需求,但还有优化空间:
- 工具发现机制:当工具数量增多时,需要更智能的工具推荐系统
- 长期记忆增强:当前RAG系统较简单,计划引入更复杂的记忆架构
- 多Agent协作:探索多个Agent分工合作的模式
- 可视化调试:开发一个交互式界面观察Agent的决策过程
这个项目让我深刻体会到,构建一个实用的AI Agent不仅需要技术能力,更需要从用户体验角度不断优化。每次看到Agent成功完成一个复杂任务时,都感觉这些努力是值得的。