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。下面我将:

  1. 保留 MySQL 记忆功能
  2. 添加一个简单 Tool 示例(比如:获取当前时间)
  3. 使用 OpenAI 的 function calling 构建带工具的 Agent
  4. 保持多会话、持久化记忆不变

✅ 修改后的完整程序: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)。

posted @ 2026-02-02 07:45  船山薪火  阅读(0)  评论(0)    收藏  举报
![image](https://img2024.cnblogs.com/blog/3174785/202601/3174785-20260125205854513-941832118.jpg)