24-1-day5-memory-agent_with_SQLite 拓展集成tools

memory-agent_with_SQLite 拓展集成tools:

一、程序功能

本程序 agent_with_sqlite_tools.py 实现了一个本地运行、具备长期记忆与工具调用能力的 AI 助手
主要功能包括:

  1. 多会话隔离对话

    • 支持多个独立会话(如 defaultjimalice
    • 通过 switch <session_id> 命令实时切换上下文
  2. 对话历史持久化

    • 所有对话记录自动保存至本地 SQLite 数据库(chat_memory.db
    • 程序重启后仍可恢复历史记忆
  3. 交互式记忆查看

    • 输入 show_memory 可查看当前会话的完整对话历史
    • 清晰区分用户与 Agent 的发言
  4. 内置工具调用能力

    • 当前已集成一个安全工具:获取当前日期和时间get_current_time
    • Agent 能在回答中主动调用该工具并整合结果(如:“当前时间是 2026-01-31 10:05:12”)
  5. 兼容 DashScope Qwen 模型

    • 通过 OpenAI 兼容接口调用通义千问 qwen-max
    • 支持函数调用(Function Calling)协议

💡 所有功能均在单机 CLI 环境下运行,无需网络服务(除调用大模型外)。


二、创建目录、虚拟环境

# 创建目录
mkdir -p day5_memory_with_tools/tools && cd day5_memory_with_tools
 
# 创建虚拟环境(Python 3.10+)
python3 -m venv mem_tools
source mem_tools/bin/activate
 
# 升级 pip 
pip install --upgrade pip -i https://mirrors.aliyun.com/pypi/simple/
 
# 创建 .env 文件
echo "DASHSCOPE_API_KEY=(替换为您的实际API Key)" > .env
 
# 安装依赖
pip install \
    langchain \
    langchain-community \
    langchain-openai \
    python-dotenv \
    -i https://mirrors.aliyun.com/pypi/simple/
   

三、完整程序

tee  agent_with_sqlite_tools.py <<'EOF'
# agent_with_sqlite_tools.py
# 功能:多会话、持久化记忆、支持工具调用的 CLI AI Agent
# 模型:通义千问 Qwen-Max(通过 DashScope OpenAI 兼容接口)
# 记忆后端:SQLite
# 工具:当前仅支持 get_current_time

import os
import sqlite3
from contextlib import contextmanager  # 用于安全管理数据库连接
from typing import List

from dotenv import load_dotenv          # 从 .env 文件加载环境变量
from langchain_openai import ChatOpenAI # 使用 OpenAI 兼容接口调用大模型
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, ToolMessage
from langchain_core.tools import Tool   # LangChain 标准工具封装
from datetime import datetime

# ==============================
# 🔽 SQLite 聊天历史(持久化记忆核心)
# ==============================

DB_FILE = "chat_memory.db"  # 本地 SQLite 数据库文件名

@contextmanager
def get_db_connection():
    """提供线程安全的 SQLite 连接上下文管理器"""
    conn = sqlite3.connect(DB_FILE, check_same_thread=False)
    try:
        yield conn
    finally:
        conn.close()

def init_db():
    """初始化数据库表结构(若不存在)"""
    with get_db_connection() as conn:
        conn.execute("""
            CREATE TABLE IF NOT EXISTS chat_history (
                session_id TEXT NOT NULL,           -- 会话标识(如 'jim')
                message_index INTEGER NOT NULL,     -- 消息序号(保证顺序)
                role TEXT NOT NULL CHECK(role IN ('human', 'ai')), -- 角色类型
                content TEXT NOT NULL,              -- 消息内容
                timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, -- 记录时间
                PRIMARY KEY (session_id, message_index)        -- 联合主键
            )
        """)
        conn.execute("CREATE INDEX IF NOT EXISTS idx_session ON chat_history(session_id);")
        conn.commit()

class SQLiteChatMessageHistory(BaseChatMessageHistory):
    """自定义聊天历史类,将对话持久化到 SQLite"""
    
    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]:
        """从数据库读取当前会话的所有消息,并转换为 LangChain 消息对象"""
        messages = []
        with get_db_connection() as conn:
            cur = conn.execute(
                "SELECT role, content FROM chat_history WHERE session_id = ? ORDER BY message_index",
                (self.session_id,)
            )
            for row in cur.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:
        """将新消息写入数据库"""
        role = "human" if isinstance(message, HumanMessage) else "ai"
        with get_db_connection() as conn:
            # 获取当前会话最大 message_index,用于生成下一个序号
            cur = conn.execute(
                "SELECT COALESCE(MAX(message_index), -1) FROM chat_history WHERE session_id = ?",
                (self.session_id,)
            )
            next_index = cur.fetchone()[0] + 1
            conn.execute(
                "INSERT INTO chat_history (session_id, message_index, role, content) VALUES (?, ?, ?, ?)",
                (self.session_id, next_index, role, message.content)
            )
            conn.commit()

    def clear(self) -> None:
        """清空当前会话的所有历史记录"""
        with get_db_connection() as conn:
            conn.execute("DELETE FROM chat_history WHERE session_id = ?", (self.session_id,))
            conn.commit()

# ==============================
# 🔽 工具定义(当前仅一个:获取当前时间)
# ==============================

def get_current_time_tool() -> str:
    """工具函数:返回格式化的当前时间字符串"""
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

# 将工具函数封装为 LangChain Tool 对象
tools = [
    Tool(
        name="get_current_time",
        func=get_current_time_tool,
        description="获取当前日期和时间,格式为 YYYY-MM-DD HH:MM:SS"
    )
]

# 构建工具名称到工具对象的映射,便于快速查找
tool_map = {tool.name: tool for tool in tools}

# ==============================
# 🔽 LLM 配置(使用 DashScope 的 Qwen-Max)
# ==============================

load_dotenv()  # 加载 .env 中的 DASHSCOPE_API_KEY

llm = ChatOpenAI(
    model="qwen-max",
    openai_api_key=os.getenv("DASHSCOPE_API_KEY"),
    openai_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",  # DashScope 兼容模式地址
    temperature=0.7  # 控制生成随机性
)

# 绑定工具,使 LLM 能在响应中生成 tool_calls
llm_with_tools = llm.bind_tools(tools)

# ==============================
# 🔽 自定义带工具调用的推理链(手动处理 Function Calling)
# ==============================

def invoke_with_tools(prompt_value) -> AIMessage:
    """
    处理带工具调用的完整推理流程:
    1. 将 Prompt 转为消息列表
    2. 首次调用 LLM(可能返回 tool_calls)
    3. 若有工具调用,执行并构造 ToolMessage
    4. 二次调用 LLM 生成最终自然语言回答
    """
    # 将 LangChain 的 ChatPromptValue 转为 BaseMessage 列表(关键步骤!)
    messages = prompt_value.to_messages()

    # 第一次调用:启用工具的 LLM
    first_response = llm_with_tools.invoke(messages)

    # 检查 LLM 是否要求调用工具
    if hasattr(first_response, 'tool_calls') and first_response.tool_calls:
        tool_messages = []
        for tool_call in first_response.tool_calls:
            tool_name = tool_call.get("name")
            tool_id = tool_call.get("id", "")  # 用于关联工具调用与结果

            if tool_name in tool_map:
                try:
                    # 执行工具函数
                    result = tool_map[tool_name].func()
                    tool_messages.append(ToolMessage(content=str(result), tool_call_id=tool_id))
                except Exception as e:
                    # 工具执行出错,也返回错误信息
                    tool_messages.append(ToolMessage(content=f"Error: {e}", tool_call_id=tool_id))
            else:
                tool_messages.append(ToolMessage(content=f"Unknown tool: {tool_name}", tool_call_id=tool_id))

        # 构造完整上下文:原始消息 + LLM 工具请求 + 工具结果
        final_messages = messages + [first_response] + tool_messages
        # 第二次调用:让 LLM 基于工具结果生成最终回答
        final_response = llm.invoke(final_messages)
        return final_response
    else:
        # 无需工具,直接返回 LLM 的回答
        return first_response


# 定义 Prompt 模板:系统提示 + 历史消息占位符 + 用户输入
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个有记忆且能使用工具的 AI 助手。请记住用户之前说过的话,并在需要时使用工具。"),
    MessagesPlaceholder(variable_name="history"),  # 此处会被自动替换为历史消息
    ("human", "{input}")  # 用户最新输入
])

# 构建推理链:Prompt → 自定义工具调用逻辑
chain = prompt | invoke_with_tools

# ==============================
# 🔽 带记忆的 Runnable(自动注入历史)
# ==============================

# 使用 RunnableWithMessageHistory 包装 chain,实现自动历史管理
with_message_history = RunnableWithMessageHistory(
    chain,
    # 工厂函数:根据 session_id 创建对应的 SQLite 历史实例
    lambda session_id: SQLiteChatMessageHistory(session_id),
    input_messages_key="input",      # 输入字典中的用户输入键
    history_messages_key="history",  # Prompt 中历史占位符的变量名
)

# ==============================
# 🔽 主程序(CLI 交互循环)
# ==============================

def show_memory(session_id: str):
    """打印当前会话的完整对话历史"""
    history = SQLiteChatMessageHistory(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("🤖 多会话记忆型 Agent 启动(SQLite + Tools 兼容版)!")
    print("🔧 已加载工具: get_current_time")
    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 "):
            new_session = user_input.split(" ", 1)[1].strip()
            if not new_session:
                print("⚠️ 用法: switch <session_id>")
                continue
            current_session = new_session
            print(f"🔄 已切换到会话: '{current_session}'\n")
            continue

        # 调用带记忆和工具的 Agent
        response = with_message_history.invoke(
            {"input": user_input},
            config={"configurable": {"session_id": current_session}}  # 传递会话ID
        )
        print(f"🤖 Agent: {response.content}\n")

EOF

四、测试

$ python agent_with_sqlite_tools.py
🤖 多会话记忆型 Agent 启动(SQLite + Tools 兼容版)!
🔧 已加载工具: get_current_time
指令:
  - 输入 'quit' 退出
  - 输入 'show_memory' 查看当前会话记忆
  - 输入 'switch <session_id>' 切换会话
  - 默认会话 ID: default

👤 [default] 你: switch jim
🔄 已切换到会话: 'jim'

👤 [jim] 你: 获取现在时间
🤖 Agent: 当前时间是2026年1月31日10点04分53秒。

👤 [jim] 你: 我是小明
🤖 Agent: 你好,小明!有什么我可以帮你的吗?

👤 [jim] 你: 我是谁,刚刚什么时间?
🤖 Agent: 你是小明。你刚才询问现在的时间,当前时间是2026年1月31日10点05分12秒。

👤 [jim] 你: quit
$ python agent_with_sqlite_tools.py
🤖 多会话记忆型 Agent 启动(SQLite + Tools 兼容版)!
🔧 已加载工具: get_current_time
指令:
  - 输入 'quit' 退出
  - 输入 'show_memory' 查看当前会话记忆
  - 输入 'switch <session_id>' 切换会话
  - 默认会话 ID: default

👤 [default] 你: show_memory
🧠 当前记忆内容:
  (无记忆)

👤 [default] 你: switch jim
🔄 已切换到会话: 'jim'

👤 [jim] 你: show_memory
🧠 当前记忆内容:
  1. 👤 用户: 获取现在时间
  2. 🤖 Agent: 当前时间是2026年1月31日10点04分53秒。
  3. 👤 用户: 我是小明
  4. 🤖 Agent: 你好,小明!有什么我可以帮你的吗?
  5. 👤 用户: 我是谁,刚刚什么时间?
  6. 🤖 Agent: 你是小明。你刚才询问现在的时间,当前时间是2026年1月31日10点05分12秒。

非常好!以下是根据你的要求,将原教案中“第1课、第2课……”的教学阶段结构,改为以程序功能模块为核心的阐述方式,即:

从最终程序出发,拆解为若干核心功能点,每个功能点说明其作用、实现关键与技术要点

这份文档可作为 项目说明文档技术复盘笔记,适合用于汇报、归档或教学回顾。


程序功能分解:多会话记忆型 AI Agent + SQLite + 工具调用

本程序 agent_with_sqlite_tools.py 实现了一个具备长期记忆、多用户会话隔离、工具调用能力的本地 AI 助手。
以下按功能模块进行分解说明。


🔹 功能 1:基础 LLM 对话能力(无记忆、无工具)

功能描述

  • 用户输入自然语言问题,Agent 调用大模型(Qwen-Max)生成回答。
  • 使用 DashScope 提供的 OpenAI 兼容接口。

🔧 实现关键

llm = ChatOpenAI(
    model="qwen-max",
    openai_api_key=os.getenv("DASHSCOPE_API_KEY"),
    openai_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1"
)

💡 技术要点

  • 通过 python-dotenv 安全加载 API Key。
  • 利用 LangChain 的 ChatOpenAI 封装兼容接口,无需手动构造 HTTP 请求。
  • 返回值为 AIMessage 对象,内容通过 .content 获取。

🔹 功能 2:对话历史注入(短期记忆)

功能描述

  • Agent 在回答时能参考当前会话的历史对话(如“我刚才说了什么?”)。
  • 历史消息自动插入 Prompt 中。

🔧 实现关键

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个有记忆的 AI 助手..."),
    MessagesPlaceholder("history"),  # ← 历史占位符
    ("human", "{input}")
])

chain = prompt | llm

💡 技术要点

  • MessagesPlaceholder("history") 是 LangChain 注入历史的标准方式。
  • 需配合 RunnableWithMessageHistory 使用,才能自动管理消息流。
  • 此阶段历史仍保存在内存中(未持久化)。

🔹 功能 3:对话持久化(SQLite 存储)

功能描述

  • 所有对话记录保存到本地 SQLite 数据库 chat_memory.db
  • 程序重启后仍能恢复历史。

🔧 实现关键

  • 自定义类 SQLiteChatMessageHistory 继承 BaseChatMessageHistory
  • 实现两个核心方法:
    • messages:从数据库读取并转为 HumanMessage / AIMessage
    • add_message:将新消息写入数据库

💡 技术要点

  • 表结构设计支持多会话:
    PRIMARY KEY (session_id, message_index)
    
  • 使用上下文管理器 @contextmanager 确保数据库连接安全关闭。
  • 消息角色(human/ai)与 LangChain 消息类型严格对应。

🔹 功能 4:多会话隔离与切换

功能描述

  • 支持多个独立会话(如 default, jim, alice)。
  • 用户可通过 switch <session_id> 切换上下文。
  • 各会话记忆互不影响。

🔧 实现关键

with_message_history = RunnableWithMessageHistory(
    chain,
    lambda session_id: SQLiteChatMessageHistory(session_id),  # ← 按 ID 创建历史实例
    input_messages_key="input",
    history_messages_key="history"
)

# 调用时传入 session_id
response = with_message_history.invoke(
    {"input": user_input},
    config={"configurable": {"session_id": current_session}}
)

💡 技术要点

  • configurable={"session_id": ...} 是 LangChain 传递会话标识的标准方式。
  • 主循环维护 current_session 变量,响应 switch 指令。
  • 新增 show_memory 命令用于调试和查看当前会话历史。

🔹 功能 5:工具调用(Function Calling)

功能描述

  • Agent 能主动调用预定义函数(如获取当前时间)。
  • 支持工具执行结果反馈给 LLM 生成自然语言回答。

🔧 实现关键

  1. 定义工具
    def get_current_time_tool(): ...
    tools = [Tool(name="get_current_time", func=..., description=...)]
    
  2. 绑定工具到 LLM
    llm_with_tools = llm.bind_tools(tools)
    
  3. 自定义调用链(两阶段):
    • 第一阶段:调用 llm_with_tools.invoke(),可能返回 tool_calls
    • 第二阶段:执行工具 → 构造 ToolMessage → 再次调用 llm.invoke()

💡 技术要点

  • 由于 Qwen 在 DashScope 上不完全兼容 OpenAI 的 Tool Calling 高级封装,必须手动处理流程。
  • 使用 tool_map 快速查找工具函数,提升可扩展性。
  • 错误处理:工具异常被捕获并转为 ToolMessage 内容,避免程序崩溃。

🧩 整体架构协同

功能 协同关系
基础对话 + 历史注入 构成最简智能体
+ SQLite 持久化 实现长期记忆
+ 多会话 支持多用户/多任务
+ 工具调用 赋予 Agent 感知外部世界的能力

协同以上功能点,实现了闭环、可交互、可扩展的本地 AI Agent 系统。


✅ 总结

本程序是框架程序,不超过1000行,但完整覆盖了现代 AI Agent 的四大核心要素:

  1. 推理引擎(LLM)
  2. 记忆系统(SQLite + 多会话)
  3. 工具使用(Function Calling)
  4. 人机交互(CLI + 指令控制)

拓展:

动态加载 tools/ 下所有 .py 文件中的工具

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