24-1-day5-memory-agent_with_SQLite 拓展集成tools
memory-agent_with_SQLite 拓展集成tools:
一、程序功能
本程序 agent_with_sqlite_tools.py 实现了一个本地运行、具备长期记忆与工具调用能力的 AI 助手,
主要功能包括:
-
多会话隔离对话
- 支持多个独立会话(如
default、jim、alice) - 通过
switch <session_id>命令实时切换上下文
- 支持多个独立会话(如
-
对话历史持久化
- 所有对话记录自动保存至本地 SQLite 数据库(
chat_memory.db) - 程序重启后仍可恢复历史记忆
- 所有对话记录自动保存至本地 SQLite 数据库(
-
交互式记忆查看
- 输入
show_memory可查看当前会话的完整对话历史 - 清晰区分用户与 Agent 的发言
- 输入
-
内置工具调用能力
- 当前已集成一个安全工具:获取当前日期和时间(
get_current_time) - Agent 能在回答中主动调用该工具并整合结果(如:“当前时间是 2026-01-31 10:05:12”)
- 当前已集成一个安全工具:获取当前日期和时间(
-
兼容 DashScope Qwen 模型
- 通过 OpenAI 兼容接口调用通义千问
qwen-max - 支持函数调用(Function Calling)协议
- 通过 OpenAI 兼容接口调用通义千问
💡 所有功能均在单机 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/AIMessageadd_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 生成自然语言回答。
🔧 实现关键
- 定义工具:
def get_current_time_tool(): ... tools = [Tool(name="get_current_time", func=..., description=...)] - 绑定工具到 LLM:
llm_with_tools = llm.bind_tools(tools) - 自定义调用链(两阶段):
- 第一阶段:调用
llm_with_tools.invoke(),可能返回tool_calls - 第二阶段:执行工具 → 构造
ToolMessage→ 再次调用llm.invoke()
- 第一阶段:调用
💡 技术要点
- 由于 Qwen 在 DashScope 上不完全兼容 OpenAI 的 Tool Calling 高级封装,必须手动处理流程。
- 使用
tool_map快速查找工具函数,提升可扩展性。 - 错误处理:工具异常被捕获并转为
ToolMessage内容,避免程序崩溃。
🧩 整体架构协同
| 功能 | 协同关系 |
|---|---|
| 基础对话 + 历史注入 | 构成最简智能体 |
| + SQLite 持久化 | 实现长期记忆 |
| + 多会话 | 支持多用户/多任务 |
| + 工具调用 | 赋予 Agent 感知外部世界的能力 |
协同以上功能点,实现了闭环、可交互、可扩展的本地 AI Agent 系统。
✅ 总结
本程序是框架程序,不超过1000行,但完整覆盖了现代 AI Agent 的四大核心要素:
- 推理引擎(LLM)
- 记忆系统(SQLite + 多会话)
- 工具使用(Function Calling)
- 人机交互(CLI + 指令控制)
拓展:
动态加载 tools/ 下所有 .py 文件中的工具
浙公网安备 33010602011771号