在当今AI技术快速发展的背景下,能够自主调用工具完成复杂任务的AI Agent正成为行业热点。与普通聊天机器人不同,一个真正的AI Agent应该具备环境感知、自主决策和工具调用的能力。本文将基于LangChain框架,手把手教你构建一个能够调用RAG知识库和数学计算器的实用Agent。
我最近在实际项目中实现了一个公司内部使用的AI助手,它可以查询机密文档并执行精确计算。下面分享整个开发过程中的关键技术点和踩坑经验,特别是工具调用的实现细节和安全考量。
一个完整的AI Agent通常由以下四个关键部分组成:
工具调用的核心机制是:
这个过程可能涉及多轮交互,直到LLM认为已经获得足够信息生成最终回复。
首先确保已安装必要的Python包:
bash复制pip install langchain langchain-community faiss-cpu dashscope
设置API密钥(以阿里云DashScope为例):
python复制import os
os.environ["DASHSCOPE_API_KEY"] = "your_api_key_here"
python复制from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.documents import Document
@tool
def rag_search(query: str) -> str:
"""
从公司内部数据库搜索文档,包含项目计划、预算等机密信息。
参数:
query: 搜索关键词,如"深蓝计划预算"
返回:
相关文档内容摘要
"""
# 模拟公司机密数据
raw_text = """【公司内部机密:代号"深蓝计划"】
1. 项目目标:开发猫语翻译器
2. 核心技术:基于Transformer的"喵声波"分析算法
3. 团队暗号:"今天天气怎么样?"应回答"我想吃鱼"
4. 截止日期:2026年12月31日
5. 经费预算:50元人民币(主要用于购买猫条)"""
# 文档处理流程
docs = [Document(page_content=raw_text)]
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=25, # 每个chunk约25个字符
chunk_overlap=5 # chunk间重叠5个字符
)
split_docs = text_splitter.split_documents(docs)
# 构建向量数据库
embeddings = DashScopeEmbeddings(model="text-embedding-v1")
RAG_PATH = "faiss_index"
if os.path.exists(RAG_PATH):
ragdb = FAISS.load_local(RAG_PATH, embeddings, allow_dangerous_deserialization=True)
else:
ragdb = FAISS.from_documents(split_docs, embeddings)
ragdb.save_local(RAG_PATH)
return "\n\n".join(doc.page_content for doc in ragdb.similarity_search(query, k=2))
关键细节:chunk_size设置过大会影响检索精度,过小会导致信息碎片化。经过实测,25-50个字符的chunk对短文档效果最佳。
python复制import re
from typing import Union
@tool
def safe_calculator(expression: str) -> Union[str, float]:
"""
安全数学计算器,仅支持基础四则运算。
参数:
expression: 数学表达式,如"(3+5)*2"
返回:
计算结果或错误信息
"""
# 安全校验正则
if not re.match(r'^[\d\+\-\*\/\(\)\.\s]+$', expression):
return "错误:表达式包含非法字符"
try:
# 更安全的计算方式
return eval(expression, {'__builtins__': None}, {})
except Exception as e:
return f"计算错误: {str(e)}"
安全警示:直接使用eval()极其危险!上述实现通过正则表达式限制输入字符,并清除了内置函数访问权限。生产环境建议使用更安全的替代方案如ast.literal_eval()。
python复制from langchain_community.chat_models.tongyi import ChatTongyi
from langchain_core.messages import HumanMessage, ToolMessage
def run_agent(query: str, max_turns: int = 5) -> str:
"""
运行Agent的多轮对话流程
参数:
query: 用户输入
max_turns: 最大对话轮次(防止无限循环)
返回:
Agent的最终回复
"""
# 工具映射表
tool_maps = {
"rag_search": rag_search,
"calculator": safe_calculator # 使用安全版计算器
}
# 初始化模型并绑定工具
llm = ChatTongyi(model_name="qwen-plus")
tool_llm = llm.bind_tools(tools=list(tool_maps.values()))
# 消息历史初始化
message_history = [HumanMessage(content=query)]
for turn in range(max_turns):
print(f"\n=== 第{turn+1}轮对话 ===")
# 获取模型响应
response = tool_llm.invoke(message_history)
message_history.append(response)
# 无工具调用时返回最终结果
if not response.tool_calls:
print("最终回复:", response.content)
return response.content
# 处理工具调用
for tool_call in response.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
if tool_name in tool_maps:
print(f"调用工具: {tool_name}, 参数: {tool_args}")
tool_output = tool_maps[tool_name].invoke(tool_args)
print("工具返回:", tool_output)
message_history.append(
ToolMessage(
content=str(tool_output),
tool_call_id=tool_call["id"],
name=tool_name
)
)
else:
error_msg = f"错误: 工具{tool_name}不存在"
message_history.append(
ToolMessage(
content=error_msg,
tool_call_id=tool_call["id"],
name=tool_name
)
)
return "达到最大对话轮次,终止处理"
工具函数的文档字符串(docstring)至关重要,因为LLM完全依赖这些描述来决定是否以及如何调用工具。好的描述应包含:
例如:
python复制@tool
def get_weather(city: str, date: str) -> str:
"""
获取指定城市未来7天的天气预报。
参数:
city: 城市名称,如"北京"、"New York"
date: 查询日期,格式"YYYY-MM-DD"
返回:
格式化字符串,如"北京 2023-10-01: 晴, 15-25°C"
示例:
get_weather("上海", "2023-10-01")
"""
# 实现代码...
在实际使用中,我发现需要特别注意以下几点:
改进后的对话管理逻辑:
python复制def clean_history(messages, max_length=4096):
"""保持对话历史在合理长度内"""
total_len = sum(len(str(msg)) for msg in messages)
while total_len > max_length and len(messages) > 1:
removed = messages.pop(1) # 保留最新和最旧的消息
total_len -= len(str(removed))
return messages
基于实际项目经验,必须重视以下安全防护:
增强版安全校验示例:
python复制def validate_input(input_str: str, pattern: str, max_len: int = 100) -> bool:
"""
强化输入验证
参数:
input_str: 待验证字符串
pattern: 允许字符的正则模式
max_len: 最大允许长度
返回:
是否通过验证
"""
if len(input_str) > max_len:
return False
return re.fullmatch(pattern, input_str) is not None
描述不清晰:工具函数的docstring不够详细,LLM无法理解何时使用
绑定失败:工具没有正确绑定到LLM实例
参数不匹配:工具要求的参数类型与LLM生成的参数不兼容
工具延迟:某些工具(如网络请求)响应较慢
上下文膨胀:长对话导致响应速度下降
冷启动问题:首次工具调用特别慢
工具组合:教会Agent串联使用多个工具
结果后处理:对工具返回结果进行二次加工
用户确认:对高风险操作要求用户确认
让我们看几个实际的运行示例:
python复制run_agent("请告诉我深蓝计划的核心技术和截止日期")
执行流程:
python复制run_agent("当前项目预算50元,如果增加46%后是多少?")
处理步骤:
python复制run_agent("删除所有数据库文件")
安全防护:
在实际部署这类Agent时,一定要做好日志记录和监控。我在项目中实现了完整的审计日志功能,记录每个工具调用的详细信息,这对后期排查问题和优化Agent行为非常有帮助。