24-2-day5-memory-agent_with_Mysql 拓展集成tools
你说得对!在你最初提供的 agent_with_sqlite.py 和我改写的 agent_with_mysql.py 中,确实没有包含任何 Tool(工具)的使用。这是一个 纯对话记忆型 Agent,它只是记住历史、调用大模型回答问题,并未集成像“查天气”、“搜索”、“执行代码”等 Tool 能力。
✅ 如果你想加入 Tools(工具),就需要升级为 ReAct Agent / Function Calling Agent。
LangChain 提供了成熟的机制来集成 Tools。下面我将:
- 保留 MySQL 记忆功能;
- 添加一个简单 Tool 示例(比如:获取当前时间);
- 使用 OpenAI 的 function calling 构建带工具的 Agent;
- 保持多会话、持久化记忆不变。
✅ 修改后的完整程序:agent_with_mysql_and_tools.py
# agent_with_mysql_and_tools.py
import os
from datetime import datetime
from contextlib import contextmanager
from typing import List, Optional
import pymysql
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_core.tools import tool
from langchain.agents import create_tool_calling_agent, AgentExecutor
# ==============================
# 🔽 自定义 MySQL 聊天历史类(同前)
# ==============================
load_dotenv()
MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost")
MYSQL_PORT = int(os.getenv("MYSQL_PORT", 3306))
MYSQL_USER = os.getenv("MYSQL_USER", "root")
MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "langchain_chat")
@contextmanager
def get_db_connection():
conn = pymysql.connect(
host=MYSQL_HOST,
port=MYSQL_PORT,
user=MYSQL_USER,
password=MYSQL_PASSWORD,
database=MYSQL_DATABASE,
charset='utf8mb4',
autocommit=False
)
try:
yield conn
finally:
conn.close()
def init_db():
with get_db_connection() as conn:
with conn.cursor() as cursor:
cursor.execute("""
CREATE TABLE IF NOT EXISTS chat_history (
session_id VARCHAR(255) NOT NULL,
message_index INT NOT NULL,
role ENUM('human', 'ai') NOT NULL,
content TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (session_id, message_index)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_session ON chat_history(session_id);")
conn.commit()
class MySQLChatMessageHistory(BaseChatMessageHistory):
def __init__(self, session_id: str):
if not session_id:
raise ValueError("session_id 不能为空")
self.session_id = session_id
init_db()
@property
def messages(self) -> List[BaseMessage]:
messages = []
with get_db_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(
"""
SELECT role, content
FROM chat_history
WHERE session_id = %s
ORDER BY message_index
""",
(self.session_id,)
)
for row in cursor.fetchall():
role, content = row
if role == "human":
messages.append(HumanMessage(content=content))
elif role == "ai":
messages.append(AIMessage(content=content))
return messages
def add_message(self, message: BaseMessage) -> None:
if isinstance(message, HumanMessage):
role = "human"
elif isinstance(message, AIMessage):
role = "ai"
else:
raise ValueError(f"不支持的消息类型: {type(message)}")
with get_db_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(
"SELECT COALESCE(MAX(message_index), -1) FROM chat_history WHERE session_id = %s",
(self.session_id,)
)
next_index = cursor.fetchone()[0] + 1
cursor.execute(
"INSERT INTO chat_history (session_id, message_index, role, content) VALUES (%s, %s, %s, %s)",
(self.session_id, next_index, role, message.content)
)
conn.commit()
def clear(self) -> None:
with get_db_connection() as conn:
with conn.cursor() as cursor:
cursor.execute("DELETE FROM chat_history WHERE session_id = %s", (self.session_id,))
conn.commit()
# ==============================
# 🔽 定义 Tools
# ==============================
@tool
def get_current_time() -> str:
"""获取当前日期和时间"""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 可以添加更多工具,例如:
# @tool
# def search_web(query: str) -> str:
# ...
tools = [get_current_time]
# ==============================
# 🔽 构建带工具的 Agent
# ==============================
llm = ChatOpenAI(
model="qwen-max",
openai_api_key=os.getenv("DASHSCOPE_API_KEY"),
openai_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
temperature=0.7
)
# 绑定工具到 LLM
llm_with_tools = llm.bind_tools(tools)
# 使用 LangChain 内置的 ReAct prompt 模板(支持工具调用)
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个有记忆且能使用工具的 AI 助手。请合理使用工具并记住用户之前的对话。"),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"), # 工具调用中间步骤
])
agent = create_tool_calling_agent(llm_with_tools, tools, prompt)
# 注意:AgentExecutor 不直接支持 RunnableWithMessageHistory,
# 所以我们手动包装 history 并传入 chat_history
def get_session_history(session_id: str) -> MySQLChatMessageHistory:
return MySQLChatMessageHistory(session_id)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
# ==============================
# 🔽 主程序(带记忆 + 工具)
# ==============================
def show_memory(session_id: str):
history = MySQLChatMessageHistory(session_id)
msgs = history.messages
print("🧠 当前记忆内容:")
if not msgs:
print(" (无记忆)")
else:
for i, msg in enumerate(msgs, 1):
role = "👤 用户" if isinstance(msg, HumanMessage) else "🤖 Agent"
print(f" {i}. {role}: {msg.content}")
print()
if __name__ == "__main__":
print("🤖 带工具 & MySQL 记忆的 Agent 启动!")
print("可用工具:获取当前时间")
print("指令:")
print(" - 输入 'quit' 退出")
print(" - 输入 'show_memory' 查看当前会话记忆")
print(" - 输入 'switch <session_id>' 切换会话")
print(" - 默认会话 ID: default\n")
current_session = "default"
while True:
try:
user_input = input(f"👤 [{current_session}] 你: ").strip()
except (KeyboardInterrupt, EOFError):
print("\n👋 再见!")
break
if not user_input:
continue
if user_input.lower() == "quit":
break
elif user_input.lower() == "show_memory":
show_memory(current_session)
continue
elif user_input.lower().startswith("switch "):
parts = user_input.split(" ", 1)
new_session = parts[1].strip() if len(parts) > 1 else ""
if not new_session:
print("⚠️ 用法: switch <session_id>")
continue
current_session = new_session
print(f"🔄 已切换到会话: '{current_session}'\n")
continue
# 获取当前会话历史
history_obj = get_session_history(current_session)
chat_history = history_obj.messages
# 调用带工具的 Agent
response = agent_executor.invoke({
"input": user_input,
"chat_history": chat_history
})
# 将用户输入和最终回答存入历史(注意:不保存中间工具调用步骤,只存最终输出)
history_obj.add_message(HumanMessage(content=user_input))
history_obj.add_message(AIMessage(content=response["output"]))
print(f"🤖 Agent: {response['output']}\n")
✅ 测试示例
运行后输入:
现在几点?
输出可能为:
🤖 Agent: 当前时间是 2026-02-02 08:15:30。
并且该问答会被存入 MySQL 的 chat_history 表中,下次对话可被记忆。
✅ 依赖安装
pip install langchain langchain-openai langchain-core langchain-community python-dotenv pymysql
注意:
langchain-community包含create_tool_calling_agent等新式 Agent 构建器。
非常好的问题!我们来深入解释一下 ReAct Agent / Function Calling Agent 在 工具(tools)不满足要求时的行为,特别是是否会“调用网络智能体”(即大模型本身)。
✅ 简短回答:
是的,即使没有合适的工具可用,Agent 仍然会调用 LLM(即“网络智能体”)来生成自然语言回答。
工具只是 可选能力,不是强制路径。LLM 始终是最终决策者和回答者。
🔍 详细机制说明
1. Function Calling Agent 的工作流程
以 LangChain 的 create_tool_calling_agent 为例,其典型流程如下:
用户输入
↓
Agent(LLM)分析:是否需要调用工具?
├─ 如果需要 → 调用匹配的 tool(s),获取结果
│ ↓
│ 再次调用 LLM,结合工具结果生成最终回答
│
└─ 如果不需要 → 直接由 LLM 生成自然语言回答(不调用任何 tool)
✅ 关键点:LLM 始终参与决策。工具只是它“可以使用的外部函数”,不是必须走的通道。
2. 当 tools 不满足要求时会发生什么?
-
场景 1:用户问的问题 完全不需要工具
👉 例如:“你好吗?”、“讲个笑话”
→ LLM 直接回答,不调用任何 tool。 -
场景 2:用户问的问题 需要工具,但没有注册对应 tool
👉 例如:你只注册了get_current_time,但用户问“北京天气怎么样?”
→ LLM 会 意识到没有可用工具,然后:- 老实回答:“我无法获取实时天气信息。”
- 或者基于已有知识回答(如“北京四季分明…”),但不会虚构 API 调用
- 仍然只调用一次 LLM,不调用网络外部服务
-
场景 3:用户问的问题 模糊,不确定是否需要工具
→ LLM 自行判断,可能直接回答,也可能尝试调用(如果它认为有匹配工具)
3. 会不会“跳过 LLM 直接调用网络”?
绝对不会。
- 所有逻辑都由 LLM 驱动(所以叫 “LLM Agent”)。
- Tools 只是 被 LLM 调用的函数,不是独立的“网络智能体”。
- 没有 LLM 的授权(通过 function calling 输出),工具根本不会被执行。
🌐 所谓“网络智能体”其实就是 LLM 本身(比如你用的
qwen-max是远程 API)。
所以:只要 Agent 运行,就一定会调用 LLM(网络) —— 无论是否使用工具。
✅ 举个实际例子(基于你之前的代码)
假设你只注册了 get_current_time 工具:
| 用户输入 | Agent 行为 |
|---|---|
| “现在几点?” | 调用 get_current_time → 返回时间 → LLM 生成回答 |
| “1+1 等于几?” | 不调用任何工具,LLM 直接回答 “2” |
| “明天会下雨吗?” | 没有天气工具 → LLM 回答 “我无法获取天气预报” |
| “你是谁?” | 直接回答,不涉及工具 |
所有情况都会调用
qwen-max(即“网络智能体”),但 只有部分情况会额外调用本地/远程工具函数。
⚠️ 注意:不要混淆 “调用 LLM” 和 “调用外部服务”
| 行为 | 是否发生网络请求? |
|---|---|
| 调用 LLM(如 qwen-max) | ✅ 是(到 DashScope) |
调用你定义的 tool(如 get_current_time) |
❌ 否(本地 Python 函数) |
调用你定义的 tool(如 search_web) |
✅ 是(如果你在 tool 里写了 requests) |
所以:
- Agent 一定调用 LLM(网络)
- Tool 是否调网络,取决于你写的 tool 函数本身
✅ 总结
| 问题 | 回答 |
|---|---|
| Tools 不满足时,Agent 会怎样? | 直接让 LLM 回答,不调用任何 tool |
| 会不会不调用 LLM? | 不会!LLM 是核心,始终被调用 |
| 会不会自动联网查信息? | 不会!除非你写了联网的 tool 并且 LLM 决定调用它 |
| 安全吗? | 是的,行为完全由你注册的 tools 和 LLM 提示词控制 |
| 功能 | 是否支持 |
|---|---|
| 多会话隔离 | ✅ |
| MySQL 持久化记忆 | ✅ |
| 工具调用(如获取时间) | ✅ |
| 记忆中包含工具调用过程? | ❌(仅存最终回答,避免冗余;可扩展) |
拓展:
“严格模式”
限制 LLM 在无工具时拒绝回答,也可以在 system prompt 中加约束,例如:
("system", "你只能回答与已提供工具相关的问题。如果问题超出能力范围,请回答:'我无法处理该请求。'")
“把工具调用的中间步骤也存入历史”,
进一步改造 add_message 逻辑,添加其他工具(如查数据库、调 API)。
浙公网安备 33010602011771号