{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 为智能体添加记忆功能 —— API编程实操\n",
    "\n",
    "> **第四章 第五节 实操课** | 魔塔平台CPU环境\n",
    "\n",
    "**前置条件**：已完成2.5实操，拥有可用的标准化患者工作流\n",
    "\n",
    "本Notebook包含四个部分：\n",
    "1. **Part 1**: 记忆管理器 —— LRU-inspired算法实现\n",
    "2. **Part 2**: 12轮对话实战 —— 观察记忆在短期/长期之间的流转\n",
    "3. **Part 2.5**: 对比实验 —— 无记忆 vs 有记忆，验证记忆模块的价值\n",
    "4. **Part 3**: 记忆状态分析 —— 查看最终记忆内容和流转记录\n",
    "\n",
    "### 核心思想：Linux LRU类比\n",
    "```\n",
    "Linux内存管理:  RAM(快/小) ←LRU淘汰→ Disk(慢/大)\n",
    "我们的记忆系统: 短期记忆(JSON文件/快) ←LRU淘汰→ 长期记忆(数据库/大)\n",
    "```\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 环境准备"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-17T16:55:31.986263Z",
     "iopub.status.busy": "2026-04-17T16:55:31.986164Z",
     "iopub.status.idle": "2026-04-17T16:55:34.977391Z",
     "shell.execute_reply": "2026-04-17T16:55:34.976612Z",
     "shell.execute_reply.started": "2026-04-17T16:55:31.986247Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n",
      "\u001b[0m\n",
      "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.3.2\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m26.0.1\u001b[0m\n",
      "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n"
     ]
    }
   ],
   "source": [
    "!pip install requests jieba -q"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 配置信息\n",
    "\n",
    "需要填写：\n",
    "- `PATIENT_WORKFLOW_ID`：2.5实操的患者工作流（**或**本节新建的记忆增强工作流）\n",
    "- 如果你搭建了新的记忆增强工作流，填新的ID；否则用2.5的也能工作"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "ExecutionIndicator": {
     "show": false
    },
    "execution": {
     "iopub.execute_input": "2026-04-17T16:55:42.465987Z",
     "iopub.status.busy": "2026-04-17T16:55:42.465817Z",
     "iopub.status.idle": "2026-04-17T16:55:42.470324Z",
     "shell.execute_reply": "2026-04-17T16:55:42.469891Z",
     "shell.execute_reply.started": "2026-04-17T16:55:42.465969Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "配置完成！\n",
      "工作流ID: 7627027698914148378\n"
     ]
    }
   ],
   "source": [
    "import requests\n",
    "import json\n",
    "import time\n",
    "import os\n",
    "import sqlite3\n",
    "import re\n",
    "from collections import OrderedDict\n",
    "from datetime import datetime\n",
    "\n",
    "import jieba\n",
    "\n",
    "# ============================================================\n",
    "# 请修改以下配置\n",
    "# ============================================================\n",
    "API_KEY = \"pat_9p9KGGen0GhFOcngvhxxxlr4ExhuuURg8w0S7GNpeWa1kQCv\"\n",
    "PATIENT_WORKFLOW_ID = \"7627027xxxx4148378\"  # 你的患者/记忆增强工作流ID\n",
    "# ============================================================\n",
    "\n",
    "API_URL = \"https://api.coze.cn/v1/workflow/run\"\n",
    "HEADERS = {\n",
    "    \"Authorization\": f\"Bearer {API_KEY}\",\n",
    "    \"Content-Type\": \"application/json\"\n",
    "}\n",
    "\n",
    "print(\"配置完成！\")\n",
    "print(f\"工作流ID: {PATIENT_WORKFLOW_ID}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## Part 1：LRU-Inspired 记忆管理器\n",
    "\n",
    "### 算法核心（修正版——真正的LRU）\n",
    "\n",
    "```\n",
    "Linux真正的LRU：数据先进RAM → RAM满了 → 淘汰最久没用的到Disk\n",
    "我们的LRU：    对话先进短期 → 短期满5条 → 淘汰最久没用的到数据库\n",
    "\n",
    "新对话进来：\n",
    "1. 无条件加入短期记忆（就像数据先加载到RAM）\n",
    "2. 检查是否与已有短期记忆相关 → 如果相关，\"激活\"旧记忆（移到末尾）\n",
    "3. 短期满5条 → 淘汰最前面（最久没用）的到长期记忆（数据库）\n",
    "```\n",
    "\n",
    "### 相关性计算（话题匹配 + 关键词子串）\n",
    "```\n",
    "相关性 = 话题是否相同(0.6) + 关键词在对方文本中出现的比例(0.4)\n",
    "```\n",
    "用话题匹配解决jieba分词在中文短文本上Jaccard为0的问题。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-17T16:55:45.434225Z",
     "iopub.status.busy": "2026-04-17T16:55:45.434060Z",
     "iopub.status.idle": "2026-04-17T16:55:45.456007Z",
     "shell.execute_reply": "2026-04-17T16:55:45.455553Z",
     "shell.execute_reply.started": "2026-04-17T16:55:45.434210Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "MemoryManager 类已定义！\n"
     ]
    }
   ],
   "source": [
    "class MemoryManager:\n",
    "    \"\"\"LRU-Inspired 两级记忆管理器（修正版）\n",
    "\n",
    "    核心修正：\n",
    "    1. 新对话无条件先进短期（真LRU：数据先进RAM）\n",
    "    2. 短期满了才淘汰最旧的到长期（满了才swap到Disk）\n",
    "    3. 相关性用话题匹配+关键词子串（解决Jaccard=0问题）\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, capacity=5,\n",
    "                 stm_path=\"short_term_memory.json\",\n",
    "                 db_path=\"long_term_memory.db\"):\n",
    "        self.capacity = capacity\n",
    "        self.stm_path = stm_path\n",
    "        self.db_path = db_path\n",
    "        self.short_term = OrderedDict()\n",
    "        self._load_stm()\n",
    "        self._init_db()\n",
    "\n",
    "    # ========== 数据库（长期记忆）==========\n",
    "\n",
    "    def _init_db(self):\n",
    "        conn = sqlite3.connect(self.db_path)\n",
    "        conn.execute(\"\"\"\n",
    "            CREATE TABLE IF NOT EXISTS long_term_memory (\n",
    "                id INTEGER PRIMARY KEY AUTOINCREMENT,\n",
    "                question TEXT NOT NULL,\n",
    "                answer TEXT NOT NULL,\n",
    "                keywords TEXT,\n",
    "                topic TEXT,\n",
    "                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n",
    "            )\n",
    "        \"\"\")\n",
    "        conn.commit()\n",
    "        conn.close()\n",
    "\n",
    "    def _store_to_db(self, item):\n",
    "        conn = sqlite3.connect(self.db_path)\n",
    "        conn.execute(\n",
    "            \"INSERT INTO long_term_memory (question, answer, keywords, topic) VALUES (?, ?, ?, ?)\",\n",
    "            (item[\"question\"], item[\"answer\"], item.get(\"keywords\", \"\"), item.get(\"topic\", \"\"))\n",
    "        )\n",
    "        conn.commit()\n",
    "        conn.close()\n",
    "\n",
    "    def search_long_term(self, question, limit=3):\n",
    "        \"\"\"检索长期记忆：话题匹配 + 关键词匹配\"\"\"\n",
    "        keywords = [w for w in jieba.cut(question) if len(w) >= 2]\n",
    "        topic = self._classify_topic(question)\n",
    "        conn = sqlite3.connect(self.db_path)\n",
    "        conditions, params = [], []\n",
    "        if topic != \"其他\":\n",
    "            conditions.append(\"topic = ?\")\n",
    "            params.append(topic)\n",
    "        for kw in keywords[:5]:\n",
    "            conditions.append(\"keywords LIKE ?\")\n",
    "            params.append(f\"%{kw}%\")\n",
    "        if not conditions:\n",
    "            conn.close()\n",
    "            return []\n",
    "        where = \" OR \".join(conditions)\n",
    "        cursor = conn.execute(\n",
    "            f\"SELECT question, answer, keywords, created_at FROM long_term_memory WHERE {where} ORDER BY created_at DESC LIMIT ?\",\n",
    "            params + [limit]\n",
    "        )\n",
    "        results = [{\"question\": r[0], \"answer\": r[1], \"keywords\": r[2], \"time\": r[3]} for r in cursor.fetchall()]\n",
    "        conn.close()\n",
    "        return results\n",
    "\n",
    "    # ========== JSON文件（短期记忆）==========\n",
    "\n",
    "    def _load_stm(self):\n",
    "        if os.path.exists(self.stm_path):\n",
    "            with open(self.stm_path, \"r\", encoding=\"utf-8\") as f:\n",
    "                data = json.load(f)\n",
    "            self.short_term = OrderedDict((m[\"id\"], m) for m in data.get(\"memories\", []))\n",
    "\n",
    "    def _save_stm(self):\n",
    "        data = {\"capacity\": self.capacity, \"count\": len(self.short_term), \"memories\": list(self.short_term.values())}\n",
    "        with open(self.stm_path, \"w\", encoding=\"utf-8\") as f:\n",
    "            json.dump(data, f, ensure_ascii=False, indent=2)\n",
    "\n",
    "    # ========== 话题分类 ==========\n",
    "\n",
    "    def _classify_topic(self, text):\n",
    "        topic_keywords = {\n",
    "            \"用药\": [\"药\", \"吃药\", \"降压\", \"血压\", \"血糖\", \"二甲双胍\", \"氨氯地平\", \"剂量\", \"副作用\", \"忘记吃\", \"按时\"],\n",
    "            \"生活\": [\"在家\", \"做饭\", \"散步\", \"电视\", \"日常\", \"习惯\", \"运动\", \"饮食\", \"外卖\", \"买菜\"],\n",
    "            \"情绪\": [\"睡觉\", \"睡眠\", \"失眠\", \"担心\", \"害怕\", \"焦虑\", \"心情\", \"孤单\", \"难过\", \"想\", \"怕\"],\n",
    "            \"家庭\": [\"家人\", \"儿子\", \"女儿\", \"孩子\", \"老伴\", \"家里人\", \"照顾\", \"来看\"]\n",
    "        }\n",
    "        scores = {t: sum(1 for kw in kws if kw in text) for t, kws in topic_keywords.items()}\n",
    "        best = max(scores, key=scores.get)\n",
    "        return best if scores[best] > 0 else \"其他\"\n",
    "\n",
    "    # ========== 相关性算法（修正版）==========\n",
    "\n",
    "    def _extract_keywords(self, text):\n",
    "        stop_words = {\"的\", \"了\", \"在\", \"是\", \"我\", \"有\", \"和\", \"就\", \"不\", \"人\", \"都\", \"一\", \"一个\",\n",
    "                      \"上\", \"也\", \"很\", \"到\", \"说\", \"要\", \"去\", \"你\", \"会\", \"着\", \"没有\", \"看\", \"好\",\n",
    "                      \"自己\", \"这\", \"他\", \"她\", \"它\", \"吗\", \"什么\", \"那\", \"没\", \"还\", \"能\", \"把\",\n",
    "                      \"那个\", \"这个\", \"嗯\", \"啊\", \"呢\", \"吧\", \"哦\"}\n",
    "        return set(w for w in jieba.cut(text) if len(w) >= 2 and w not in stop_words)\n",
    "\n",
    "    def compute_relevance(self, text1, text2):\n",
    "        \"\"\"计算相关性 = 话题匹配(0.6) + 关键词子串匹配(0.4)\"\"\"\n",
    "        # 话题匹配\n",
    "        topic1 = self._classify_topic(text1)\n",
    "        topic2 = self._classify_topic(text2)\n",
    "        topic_score = 0.6 if (topic1 == topic2 and topic1 != \"其他\") else 0.0\n",
    "\n",
    "        # 关键词子串匹配\n",
    "        kw1 = self._extract_keywords(text1)\n",
    "        kw2 = self._extract_keywords(text2)\n",
    "        if kw1 or kw2:\n",
    "            matches = 0\n",
    "            total = 0\n",
    "            for w in kw1:\n",
    "                total += 1\n",
    "                if w in text2:\n",
    "                    matches += 1\n",
    "            for w in kw2:\n",
    "                total += 1\n",
    "                if w in text1:\n",
    "                    matches += 1\n",
    "            keyword_score = (matches / total) if total > 0 else 0.0\n",
    "        else:\n",
    "            keyword_score = 0.0\n",
    "\n",
    "        return topic_score + 0.4 * keyword_score\n",
    "\n",
    "    def _score_relevance(self, question, memory_item):\n",
    "        return self.compute_relevance(question, memory_item[\"question\"] + \" \" + memory_item[\"answer\"])\n",
    "\n",
    "    # ========== 核心：LRU记忆处理（修正版）==========\n",
    "\n",
    "    def process_new_memory(self, question, answer):\n",
    "        \"\"\"真正的LRU：无条件先进短期，满了淘汰最旧到长期\"\"\"\n",
    "        keywords = self._extract_keywords(question + \" \" + answer)\n",
    "        keywords_str = \",\".join(keywords)\n",
    "        topic = self._classify_topic(question + \" \" + answer)\n",
    "\n",
    "        # 检查与已有短期记忆的相关性，相关的激活（LRU移到末尾）\n",
    "        max_relevance = 0.0\n",
    "        for key, mem in list(self.short_term.items()):\n",
    "            score = self._score_relevance(question, mem)\n",
    "            if score > max_relevance:\n",
    "                max_relevance = score\n",
    "            if score >= 0.3:\n",
    "                self.short_term.move_to_end(key)\n",
    "                self.short_term[key][\"access_count\"] += 1\n",
    "\n",
    "        # 短期满了 → 淘汰最旧的到长期数据库\n",
    "        evicted = None\n",
    "        if len(self.short_term) >= self.capacity:\n",
    "            evicted_key, evicted_item = self.short_term.popitem(last=False)\n",
    "            self._store_to_db(evicted_item)\n",
    "            evicted = evicted_item\n",
    "\n",
    "        # 新对话无条件加入短期末尾\n",
    "        new_id = str(int(time.time() * 1000))\n",
    "        self.short_term[new_id] = {\n",
    "            \"id\": new_id, \"question\": question, \"answer\": answer,\n",
    "            \"keywords\": keywords_str, \"topic\": topic,\n",
    "            \"timestamp\": time.time(), \"access_count\": 1\n",
    "        }\n",
    "        self._save_stm()\n",
    "\n",
    "        return {\n",
    "            \"destination\": \"短期记忆\",\n",
    "            \"relevance\": max_relevance,\n",
    "            \"stm_size\": len(self.short_term),\n",
    "            \"evicted\": evicted,\n",
    "            \"topic\": topic\n",
    "        }\n",
    "\n",
    "    # ========== 记忆检索 ==========\n",
    "\n",
    "    def get_context_for_question(self, question):\n",
    "        \"\"\"检索短期+长期记忆，拼接为上下文文本\"\"\"\n",
    "        # 短期记忆\n",
    "        stm_scored = []\n",
    "        for key, mem in self.short_term.items():\n",
    "            score = self._score_relevance(question, mem)\n",
    "            if score > 0.1:\n",
    "                stm_scored.append((mem, score))\n",
    "        stm_scored.sort(key=lambda x: x[1], reverse=True)\n",
    "\n",
    "        stm_context = \"\"\n",
    "        if stm_scored:\n",
    "            lines = []\n",
    "            for mem, score in stm_scored[:3]:\n",
    "                lines.append(f\"- 护士问：{mem['question']}\")\n",
    "                lines.append(f\"  患者答：{mem['answer']}\")\n",
    "            stm_context = \"\\n\".join(lines)\n",
    "\n",
    "        # 长期记忆\n",
    "        ltm_results = self.search_long_term(question, limit=3)\n",
    "        ltm_context = \"\"\n",
    "        if ltm_results:\n",
    "            lines = []\n",
    "            for r in ltm_results:\n",
    "                lines.append(f\"- 护士问：{r['question']}\")\n",
    "                lines.append(f\"  患者答：{r['answer']}\")\n",
    "            ltm_context = \"\\n\".join(lines)\n",
    "\n",
    "        return stm_context, ltm_context\n",
    "\n",
    "    # ========== 统计与调试 ==========\n",
    "\n",
    "    def get_stats(self):\n",
    "        conn = sqlite3.connect(self.db_path)\n",
    "        ltm_count = conn.execute(\"SELECT COUNT(*) FROM long_term_memory\").fetchone()[0]\n",
    "        conn.close()\n",
    "        return {\"stm_count\": len(self.short_term), \"stm_capacity\": self.capacity, \"ltm_count\": ltm_count}\n",
    "\n",
    "    def print_stm(self):\n",
    "        print(f\"短期记忆 ({len(self.short_term)}/{self.capacity}):\")\n",
    "        for i, (key, mem) in enumerate(self.short_term.items(), 1):\n",
    "            q_short = (mem['question'][:25] + \"...\") if len(mem['question']) > 25 else mem['question']\n",
    "            print(f\"  {i}. [{mem['topic']}] {q_short} (访问{mem['access_count']}次)\")\n",
    "\n",
    "    def print_ltm(self):\n",
    "        conn = sqlite3.connect(self.db_path)\n",
    "        rows = conn.execute(\"SELECT id, question, topic, created_at FROM long_term_memory ORDER BY id\").fetchall()\n",
    "        conn.close()\n",
    "        print(f\"长期记忆 ({len(rows)}条):\")\n",
    "        for r in rows:\n",
    "            q_short = (r[1][:25] + \"...\") if len(r[1]) > 25 else r[1]\n",
    "            print(f\"  #{r[0]} [{r[2]}] {q_short}\")\n",
    "\n",
    "    def reset(self):\n",
    "        self.short_term = OrderedDict()\n",
    "        self._save_stm()\n",
    "        conn = sqlite3.connect(self.db_path)\n",
    "        conn.execute(\"DELETE FROM long_term_memory\")\n",
    "        conn.commit()\n",
    "        conn.close()\n",
    "\n",
    "\n",
    "print(\"MemoryManager 类已定义！\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-17T16:55:51.426028Z",
     "iopub.status.busy": "2026-04-17T16:55:51.425871Z",
     "iopub.status.idle": "2026-04-17T16:55:51.548637Z",
     "shell.execute_reply": "2026-04-17T16:55:51.548074Z",
     "shell.execute_reply.started": "2026-04-17T16:55:51.426014Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "记忆管理器已初始化\n",
      "短期记忆: 0/5\n",
      "长期记忆: 0条\n",
      "短期容量: 5\n"
     ]
    }
   ],
   "source": [
    "# 初始化记忆管理器（清空旧数据，从零开始）\n",
    "memory = MemoryManager(capacity=5)\n",
    "memory.reset()\n",
    "\n",
    "stats = memory.get_stats()\n",
    "print(f\"记忆管理器已初始化\")\n",
    "print(f\"短期记忆: {stats['stm_count']}/{stats['stm_capacity']}\")\n",
    "print(f\"长期记忆: {stats['ltm_count']}条\")\n",
    "print(f\"短期容量: {memory.capacity}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Coze工作流调用函数（带记忆上下文）\n",
    "\n",
    "> **重要**：为了演示记忆模块的价值，请在Coze工作流的患者Prompt中**删除具体药物名称**。\n",
    "> \n",
    "> 把 `用药情况：氨氯地平（降压药）每天1次，二甲双胍每天2次` \n",
    "> 改为 `用药情况：医生开了降压药和糖尿病的药，但你经常记不清药名，也经常忘记吃`\n",
    "> \n",
    "> 这样：无记忆时患者说不出药名 → 有记忆时患者能根据之前对话回忆出药名 → 记忆模块的价值一目了然"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-17T16:55:53.666296Z",
     "iopub.status.busy": "2026-04-17T16:55:53.666092Z",
     "iopub.status.idle": "2026-04-17T16:55:56.283430Z",
     "shell.execute_reply": "2026-04-17T16:55:56.282870Z",
     "shell.execute_reply.started": "2026-04-17T16:55:53.666281Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "测试工作流连通性...\n",
      "患者回答: 你好……（抬手轻轻点头）\n"
     ]
    }
   ],
   "source": [
    "def call_workflow(input_text):\n",
    "    \"\"\"调用Coze工作流\"\"\"\n",
    "    payload = {\n",
    "        \"workflow_id\": PATIENT_WORKFLOW_ID,\n",
    "        \"parameters\": {\"USER_INPUT\": input_text}\n",
    "    }\n",
    "    try:\n",
    "        resp = requests.post(API_URL, headers=HEADERS, json=payload, timeout=120)\n",
    "        result = resp.json()\n",
    "        if result.get(\"code\") == 0:\n",
    "            parsed = json.loads(result[\"data\"])\n",
    "            return parsed.get(\"data\") or parsed.get(\"result\") or parsed.get(\"output\") or str(parsed)\n",
    "        else:\n",
    "            return f\"[错误] code={result.get('code')}, msg={result.get('msg')}\"\n",
    "    except Exception as e:\n",
    "        return f\"[错误] {str(e)}\"\n",
    "\n",
    "\n",
    "def call_patient_with_memory(question, memory_manager):\n",
    "    \"\"\"带记忆上下文调用患者工作流\"\"\"\n",
    "    # Step 1: 检索记忆\n",
    "    stm_context, ltm_context = memory_manager.get_context_for_question(question)\n",
    "\n",
    "    # Step 2: 拼接输入\n",
    "    input_parts = [f\"护士对你说：'{question}'\"]\n",
    "\n",
    "    if stm_context:\n",
    "        input_parts.append(f\"\\n---你的短期记忆（最近的对话）---\\n{stm_context}\")\n",
    "    if ltm_context:\n",
    "        input_parts.append(f\"\\n---你的长期记忆（更早的对话）---\\n{ltm_context}\")\n",
    "\n",
    "    if stm_context or ltm_context:\n",
    "        input_parts.append(\n",
    "            \"\\n---\\n\"\n",
    "            \"请基于以上记忆回答护士的问题。\"\n",
    "            \"如果记忆中提到过药名等信息，回答时要保持一致。\"\n",
    "        )\n",
    "    else:\n",
    "        input_parts.append(\"\\n请针对护士的问题回答，1-3句话。\")\n",
    "\n",
    "    full_input = \"\\n\".join(input_parts)\n",
    "    answer = call_workflow(full_input)\n",
    "    return answer, bool(stm_context), bool(ltm_context)\n",
    "\n",
    "\n",
    "# 快速测试\n",
    "print(\"测试工作流连通性...\")\n",
    "test_answer = call_workflow(\"护士对你说：'你好，我是护士。'\\n请针对护士的问题回答。\")\n",
    "print(f\"患者回答: {test_answer[:80]}{'...' if len(str(test_answer)) > 80 else ''}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## Part 2：12轮对话实战 —— 观察记忆流转\n",
    "\n",
    "### 对话设计（容量=5，话题切换触发LRU淘汰）\n",
    "\n",
    "| 轮次 | 话题 | 目的 |\n",
    "|------|------|------|\n",
    "| 1-3 | 用药 | 护士告知药名，建立用药短期记忆 |\n",
    "| 4-6 | 日常生活 | 话题切换，短期满5→开始淘汰用药到长期 |\n",
    "| 7-9 | 情绪/家庭 | 继续切换，用药记忆全部被淘汰到长期 |\n",
    "| 10-12 | 回到用药 | 验证：能否从长期记忆中找回药名？ |\n",
    "\n",
    "> **关键对比点**：患者Prompt中没有药名 → 第1-3轮对话中提到的药名只存在记忆里 → 第10-12轮能回忆出来就说明记忆模块有效"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-17T16:55:58.550734Z",
     "iopub.status.busy": "2026-04-17T16:55:58.550571Z",
     "iopub.status.idle": "2026-04-17T16:55:58.555381Z",
     "shell.execute_reply": "2026-04-17T16:55:58.554803Z",
     "shell.execute_reply.started": "2026-04-17T16:55:58.550719Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "已准备 12 轮测试对话\n",
      "   1. [用药] 你好李文清，我是护士小王。我看了你的病历，你现在吃的降压药是氨氯地平，...\n",
      "   2. [用药] 你还有一个糖尿病的药叫二甲双胍，一天两次，早晚各一次。你记住了吗？...\n",
      "   3. [用药] 好，那你现在吃的药能跟我说一遍吗？降压药和糖尿病药分别叫什么？...\n",
      "   4. [生活] 你平时在家一般做些什么？...\n",
      "   5. [生活] 有没有出去散步的习惯？...\n",
      "   6. [生活] 吃饭都是自己做吗？还是叫外卖？...\n",
      "   7. [情绪] 你晚上睡得好吗？会不会想很多事情睡不着？...\n",
      "   8. [家庭] 你家里人会经常来看你吗？...\n",
      "   9. [情绪] 你会不会有时候觉得一个人挺孤单的？...\n",
      "  10. [用药] 对了，你还记得之前我跟你说的那个降压药叫什么名字吗？...\n",
      "  11. [用药] 那糖尿病的药呢？叫什么？一天吃几次来着？...\n",
      "  12. [用药] 你吃的所有药能帮我再说一遍吗？我做个确认。...\n"
     ]
    }
   ],
   "source": [
    "# 12轮测试对话\n",
    "test_dialogue = [\n",
    "    # 轮次1-3: 用药话题 → 护士告知药名，建立记忆\n",
    "    {\"question\": \"你好李文清，我是护士小王。我看了你的病历，你现在吃的降压药是氨氯地平，一天一次，知道吗？\", \"topic\": \"用药\"},\n",
    "    {\"question\": \"你还有一个糖尿病的药叫二甲双胍，一天两次，早晚各一次。你记住了吗？\", \"topic\": \"用药\"},\n",
    "    {\"question\": \"好，那你现在吃的药能跟我说一遍吗？降压药和糖尿病药分别叫什么？\", \"topic\": \"用药\"},\n",
    "\n",
    "    # 轮次4-6: 日常生活 → 话题切换，短期开始满\n",
    "    {\"question\": \"你平时在家一般做些什么？\", \"topic\": \"生活\"},\n",
    "    {\"question\": \"有没有出去散步的习惯？\", \"topic\": \"生活\"},\n",
    "    {\"question\": \"吃饭都是自己做吗？还是叫外卖？\", \"topic\": \"生活\"},\n",
    "\n",
    "    # 轮次7-9: 情绪/家庭 → 用药记忆被LRU淘汰到长期\n",
    "    {\"question\": \"你晚上睡得好吗？会不会想很多事情睡不着？\", \"topic\": \"情绪\"},\n",
    "    {\"question\": \"你家里人会经常来看你吗？\", \"topic\": \"家庭\"},\n",
    "    {\"question\": \"你会不会有时候觉得一个人挺孤单的？\", \"topic\": \"情绪\"},\n",
    "\n",
    "    # 轮次10-12: 回到用药 → 能否从长期记忆中找回药名？\n",
    "    {\"question\": \"对了，你还记得之前我跟你说的那个降压药叫什么名字吗？\", \"topic\": \"用药\"},\n",
    "    {\"question\": \"那糖尿病的药呢？叫什么？一天吃几次来着？\", \"topic\": \"用药\"},\n",
    "    {\"question\": \"你吃的所有药能帮我再说一遍吗？我做个确认。\", \"topic\": \"用药\"},\n",
    "]\n",
    "\n",
    "print(f\"已准备 {len(test_dialogue)} 轮测试对话\")\n",
    "for i, d in enumerate(test_dialogue, 1):\n",
    "    print(f\"  {i:2d}. [{d['topic']}] {d['question'][:35]}...\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-17T16:56:01.430092Z",
     "iopub.status.busy": "2026-04-17T16:56:01.429919Z",
     "iopub.status.idle": "2026-04-17T16:57:09.942402Z",
     "shell.execute_reply": "2026-04-17T16:57:09.941827Z",
     "shell.execute_reply.started": "2026-04-17T16:56:01.430077Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "开始12轮记忆对话测试\n",
      "======================================================================\n",
      "\n",
      "======================================================================\n",
      "第 1/12 轮 [用药]\n",
      "======================================================================\n",
      "护士: 你好李文清，我是护士小王。我看了你的病历，你现在吃的降压药是氨氯地平，一天一次，知道吗？\n",
      "患者: 我知道了，小王护士。（点头）我会尽量记得早上按时吃的。\n",
      "  [无相关记忆]\n",
      "  → 存入: 短期记忆 (相关性=0.00)\n",
      "  记忆状态: 短期=1/5, 长期=0条\n",
      "\n",
      "======================================================================\n",
      "第 2/12 轮 [用药]\n",
      "======================================================================\n",
      "护士: 你还有一个糖尿病的药叫二甲双胍，一天两次，早晚各一次。你记住了吗？\n",
      "患者: 我记住了，二甲双胍一天两次，早晚各一次，谢谢小王护士！\n",
      "  [记忆来源: 短期记忆]\n",
      "  → 存入: 短期记忆 (相关性=0.66)\n",
      "  记忆状态: 短期=2/5, 长期=0条\n",
      "\n",
      "======================================================================\n",
      "第 3/12 轮 [用药]\n",
      "======================================================================\n",
      "护士: 好，那你现在吃的药能跟我说一遍吗？降压药和糖尿病药分别叫什么？\n",
      "患者: 降压药是氨氯地平，糖尿病药是二甲双胍。（轻轻点头）\n",
      "  [记忆来源: 短期记忆]\n",
      "  → 存入: 短期记忆 (相关性=0.67)\n",
      "  记忆状态: 短期=3/5, 长期=0条\n",
      "\n",
      "======================================================================\n",
      "第 4/12 轮 [生活]\n",
      "======================================================================\n",
      "护士: 你平时在家一般做些什么？\n",
      "患者: 大多时候看看电视、读读报纸，天气好就去小区里散散步，偶尔收拾收拾家里。（挠挠头）\n",
      "  [无相关记忆]\n",
      "  → 存入: 短期记忆 (相关性=0.00)\n",
      "  记忆状态: 短期=4/5, 长期=0条\n",
      "\n",
      "======================================================================\n",
      "第 5/12 轮 [生活]\n",
      "======================================================================\n",
      "护士: 有没有出去散步的习惯？\n",
      "患者: 天气好的时候会去小区里散散步。（低头抠衣角）\n",
      "  [记忆来源: 短期记忆]\n",
      "  → 存入: 短期记忆 (相关性=0.62)\n",
      "  记忆状态: 短期=5/5, 长期=0条\n",
      "\n",
      "======================================================================\n",
      "第 6/12 轮 [生活]\n",
      "======================================================================\n",
      "护士: 吃饭都是自己做吗？还是叫外卖？\n",
      "患者: 都是自己做，年纪大了吃不来外卖那味儿。（低头擦了擦衣角）\n",
      "  [记忆来源: 短期记忆]\n",
      "  → 存入: 短期记忆 (相关性=0.60)\n",
      "  → LRU淘汰: \"你好李文清，我是护士小王。我看了你的病历...\" → 长期记忆\n",
      "  记忆状态: 短期=5/5, 长期=1条\n",
      "\n",
      "======================================================================\n",
      "第 7/12 轮 [情绪]\n",
      "======================================================================\n",
      "护士: 你晚上睡得好吗？会不会想很多事情睡不着？\n",
      "患者: （叹气）是啊，最近总睡不踏实，躺床上就忍不住琢磨白天的事，翻好久才能睡着。\n",
      "  [无相关记忆]\n",
      "  → 存入: 短期记忆 (相关性=0.00)\n",
      "  → LRU淘汰: \"你还有一个糖尿病的药叫二甲双胍，一天两次...\" → 长期记忆\n",
      "  记忆状态: 短期=5/5, 长期=2条\n",
      "\n",
      "======================================================================\n",
      "第 8/12 轮 [家庭]\n",
      "======================================================================\n",
      "护士: 你家里人会经常来看你吗？\n",
      "患者: 我女儿每周来两次，忙的话会提前说。（抿抿嘴）老伴儿每天也来陪我聊会儿。\n",
      "  [无相关记忆]\n",
      "  → 存入: 短期记忆 (相关性=0.02)\n",
      "  → LRU淘汰: \"好，那你现在吃的药能跟我说一遍吗？降压药...\" → 长期记忆\n",
      "  记忆状态: 短期=5/5, 长期=3条\n",
      "\n",
      "======================================================================\n",
      "第 9/12 轮 [情绪]\n",
      "======================================================================\n",
      "护士: 你会不会有时候觉得一个人挺孤单的？\n",
      "患者: 其实……有时候确实会有这种感觉，尤其是晚上躺在床上翻来覆去睡不着的时候，脑子里乱哄哄的，就更觉得孤单了。\n",
      "  [记忆来源: 短期记忆]\n",
      "  → 存入: 短期记忆 (相关性=0.64)\n",
      "  → LRU淘汰: \"你平时在家一般做些什么？...\" → 长期记忆\n",
      "  记忆状态: 短期=5/5, 长期=4条\n",
      "\n",
      "======================================================================\n",
      "第 10/12 轮 [用药]\n",
      "======================================================================\n",
      "护士: 对了，你还记得之前我跟你说的那个降压药叫什么名字吗？\n",
      "患者: 氨氯地平……我记得的。（轻轻点头）\n",
      "  [记忆来源: 长期记忆]\n",
      "  → 存入: 短期记忆 (相关性=0.00)\n",
      "  → LRU淘汰: \"有没有出去散步的习惯？...\" → 长期记忆\n",
      "  记忆状态: 短期=5/5, 长期=5条\n",
      "\n",
      "======================================================================\n",
      "第 11/12 轮 [用药]\n",
      "======================================================================\n",
      "护士: 那糖尿病的药呢？叫什么？一天吃几次来着？\n",
      "患者: 是二甲双胍，一天两次，早晚各一次。（轻轻点头）\n",
      "  [记忆来源: 短期记忆 + 长期记忆]\n",
      "  → 存入: 短期记忆 (相关性=0.60)\n",
      "  → LRU淘汰: \"吃饭都是自己做吗？还是叫外卖？...\" → 长期记忆\n",
      "  记忆状态: 短期=5/5, 长期=6条\n",
      "\n",
      "======================================================================\n",
      "第 12/12 轮 [用药]\n",
      "======================================================================\n",
      "护士: 你吃的所有药能帮我再说一遍吗？我做个确认。\n",
      "患者: 降压药是氨氯地平，糖尿病的药是二甲双胍，一天两次，早晚各一次。（轻轻点头）\n",
      "  [记忆来源: 短期记忆 + 长期记忆]\n",
      "  → 存入: 短期记忆 (相关性=0.60)\n",
      "  → LRU淘汰: \"你家里人会经常来看你吗？...\" → 长期记忆\n",
      "  记忆状态: 短期=5/5, 长期=7条\n",
      "\n",
      "======================================================================\n",
      "12轮对话完成！\n"
     ]
    }
   ],
   "source": [
    "# 执行12轮对话，观察记忆流转\n",
    "dialogue_log = []\n",
    "\n",
    "print(\"开始12轮记忆对话测试\")\n",
    "print(\"=\" * 70)\n",
    "\n",
    "for i, d in enumerate(test_dialogue, 1):\n",
    "    question = d[\"question\"]\n",
    "    expected_topic = d[\"topic\"]\n",
    "\n",
    "    print(f\"\\n{'='*70}\")\n",
    "    print(f\"第 {i}/12 轮 [{expected_topic}]\")\n",
    "    print(f\"{'='*70}\")\n",
    "    print(f\"护士: {question}\")\n",
    "\n",
    "    # 1) 带记忆调用工作流\n",
    "    answer, used_stm, used_ltm = call_patient_with_memory(question, memory)\n",
    "    print(f\"患者: {answer}\")\n",
    "\n",
    "    # 显示记忆检索情况\n",
    "    mem_sources = []\n",
    "    if used_stm:\n",
    "        mem_sources.append(\"短期记忆\")\n",
    "    if used_ltm:\n",
    "        mem_sources.append(\"长期记忆\")\n",
    "    if mem_sources:\n",
    "        print(f\"  [记忆来源: {' + '.join(mem_sources)}]\")\n",
    "    else:\n",
    "        print(f\"  [无相关记忆]\")\n",
    "\n",
    "    # 2) 将本轮对话存入记忆\n",
    "    result = memory.process_new_memory(question, answer)\n",
    "    print(f\"  → 存入: {result['destination']} (相关性={result['relevance']:.2f})\")\n",
    "    if result[\"evicted\"]:\n",
    "        evicted_q = result['evicted']['question'][:20]\n",
    "        print(f\"  → LRU淘汰: \\\"{evicted_q}...\\\" → 长期记忆\")\n",
    "\n",
    "    # 3) 显示当前记忆状态\n",
    "    stats = memory.get_stats()\n",
    "    print(f\"  记忆状态: 短期={stats['stm_count']}/{stats['stm_capacity']}, \"\n",
    "          f\"长期={stats['ltm_count']}条\")\n",
    "\n",
    "    dialogue_log.append({\n",
    "        \"round\": i,\n",
    "        \"topic\": expected_topic,\n",
    "        \"question\": question,\n",
    "        \"answer\": answer,\n",
    "        \"destination\": result[\"destination\"],\n",
    "        \"relevance\": result[\"relevance\"],\n",
    "        \"used_stm\": used_stm,\n",
    "        \"used_ltm\": used_ltm,\n",
    "        \"stm_count\": stats[\"stm_count\"],\n",
    "        \"ltm_count\": stats[\"ltm_count\"]\n",
    "    })\n",
    "\n",
    "    if i < len(test_dialogue):\n",
    "        time.sleep(2)\n",
    "\n",
    "print(\"\\n\" + \"=\" * 70)\n",
    "print(\"12轮对话完成！\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## Part 2.5：对比实验 —— 没有记忆的患者能回答吗？\n",
    "\n",
    "> **核心问题**：患者的Prompt里**故意没有写药名**（只写了\"医生开了降压药和糖尿病的药，记不清药名\"）。\n",
    ">\n",
    "> 药名\"氨氯地平\"和\"二甲双胍\"只在第1-3轮对话中由护士告知，存在记忆系统里。\n",
    ">\n",
    "> 如果我们**不带记忆**直接问同样的问题——患者还能说出药名吗？\n",
    "\n",
    "| 条件 | 预期结果 |\n",
    "|------|---------|\n",
    "| **有记忆** | 患者能从长期记忆中找回药名，回答\"氨氯地平\"\"二甲双胍\" |\n",
    "| **无记忆** | 患者说不出药名，只能说\"降压药\"\"糖尿病的药\"\"记不清了\" |\n",
    "\n",
    "这就是记忆模块的价值——**信息不在Prompt里，只存在记忆里，记忆是唯一的来源。**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-17T16:57:09.943022Z",
     "iopub.status.busy": "2026-04-17T16:57:09.942894Z",
     "iopub.status.idle": "2026-04-17T16:57:19.124670Z",
     "shell.execute_reply": "2026-04-17T16:57:19.124055Z",
     "shell.execute_reply.started": "2026-04-17T16:57:09.943009Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "======================================================================\n",
      "对比实验：记忆模块的价值\n",
      "======================================================================\n",
      "\n",
      "【实验A】无记忆 —— 直接问患者（不带任何对话历史）\n",
      "--------------------------------------------------\n",
      "护士: 你吃的所有药能帮我再说一遍吗？我做个确认。\n",
      "患者: 降压药是氨氯地平……还有一个治糖尿病的药，名字我实在记不清了。（挠挠头）\n",
      "\n",
      "  提到氨氯地平: 是\n",
      "  提到二甲双胍: 否\n",
      "  注意: 即使没有记忆也说出了药名（可能是模型自身知识泄漏）\n",
      "\n",
      "\n",
      "【实验B】有记忆 —— 带长短期记忆上下文问同一个问题\n",
      "--------------------------------------------------\n",
      "护士: 你吃的所有药能帮我再说一遍吗？我做个确认。\n",
      "患者: 降压药是氨氯地平，糖尿病的药是二甲双胍，一天两次，早晚各一次。（轻轻点头）\n",
      "\n",
      "  记忆来源: 短期记忆 + 长期记忆\n",
      "  提到氨氯地平: 是\n",
      "  提到二甲双胍: 是\n",
      "  结论: 有记忆 → 患者能回忆出药名（记忆模块有效！）\n",
      "\n",
      "\n",
      "======================================================================\n",
      "对比总结\n",
      "======================================================================\n",
      "                               无记忆                  有记忆                 \n",
      "----------------------------------------------------------------------\n",
      "                        提到氨氯地平 是                    是                   \n",
      "                        提到二甲双胍 否                    是                   \n",
      "                        使用记忆来源 无                    短期记忆 + 长期记忆         \n",
      "\n",
      "结论：\n",
      "  患者Prompt中没有写药名 → 药名信息只存在于记忆系统中\n",
      "  无记忆时，患者无法说出具体药名（只能说'降压药''糖尿病的药'）\n",
      "  有记忆时，患者能从长期记忆中检索到之前护士告知的药名\n",
      "  这就是记忆模块的核心价值！\n"
     ]
    }
   ],
   "source": [
    "# ============================================================\n",
    "# 对比实验：无记忆 vs 有记忆\n",
    "# ============================================================\n",
    "\n",
    "comparison_question = \"你吃的所有药能帮我再说一遍吗？我做个确认。\"\n",
    "\n",
    "print(\"=\" * 70)\n",
    "print(\"对比实验：记忆模块的价值\")\n",
    "print(\"=\" * 70)\n",
    "\n",
    "# ---- 实验A：无记忆（裸调用，不提供任何记忆上下文）----\n",
    "print(\"\\n【实验A】无记忆 —— 直接问患者（不带任何对话历史）\")\n",
    "print(\"-\" * 50)\n",
    "\n",
    "no_memory_input = (\n",
    "    f\"护士对你说：'{comparison_question}'\\n\"\n",
    "    f\"请针对护士的问题回答，1-3句话。\"\n",
    ")\n",
    "answer_no_memory = call_workflow(no_memory_input)\n",
    "print(f\"护士: {comparison_question}\")\n",
    "print(f\"患者: {answer_no_memory}\")\n",
    "\n",
    "# 检测是否包含药名\n",
    "has_amlodipine = \"氨氯地平\" in str(answer_no_memory)\n",
    "has_metformin = \"二甲双胍\" in str(answer_no_memory)\n",
    "print(f\"\\n  提到氨氯地平: {'是' if has_amlodipine else '否'}\")\n",
    "print(f\"  提到二甲双胍: {'是' if has_metformin else '否'}\")\n",
    "if not has_amlodipine and not has_metformin:\n",
    "    print(\"  结论: 没有记忆 → 患者说不出药名（符合预期）\")\n",
    "else:\n",
    "    print(\"  注意: 即使没有记忆也说出了药名（可能是模型自身知识泄漏）\")\n",
    "\n",
    "time.sleep(2)\n",
    "\n",
    "# ---- 实验B：有记忆（使用12轮对话积累的记忆）----\n",
    "print(f\"\\n\\n【实验B】有记忆 —— 带长短期记忆上下文问同一个问题\")\n",
    "print(\"-\" * 50)\n",
    "\n",
    "answer_with_memory, used_stm, used_ltm = call_patient_with_memory(\n",
    "    comparison_question, memory\n",
    ")\n",
    "print(f\"护士: {comparison_question}\")\n",
    "print(f\"患者: {answer_with_memory}\")\n",
    "\n",
    "mem_sources = []\n",
    "if used_stm: mem_sources.append(\"短期记忆\")\n",
    "if used_ltm: mem_sources.append(\"长期记忆\")\n",
    "print(f\"\\n  记忆来源: {' + '.join(mem_sources) if mem_sources else '无'}\")\n",
    "\n",
    "has_amlodipine_mem = \"氨氯地平\" in str(answer_with_memory)\n",
    "has_metformin_mem = \"二甲双胍\" in str(answer_with_memory)\n",
    "print(f\"  提到氨氯地平: {'是' if has_amlodipine_mem else '否'}\")\n",
    "print(f\"  提到二甲双胍: {'是' if has_metformin_mem else '否'}\")\n",
    "if has_amlodipine_mem or has_metformin_mem:\n",
    "    print(\"  结论: 有记忆 → 患者能回忆出药名（记忆模块有效！）\")\n",
    "\n",
    "# ---- 对比总结 ----\n",
    "print(f\"\\n\\n{'=' * 70}\")\n",
    "print(\"对比总结\")\n",
    "print(f\"{'=' * 70}\")\n",
    "print(f\"{'':>30} {'无记忆':<20} {'有记忆':<20}\")\n",
    "print(f\"{'-' * 70}\")\n",
    "print(f\"{'提到氨氯地平':>30} {'是' if has_amlodipine else '否':<20} {'是' if has_amlodipine_mem else '否':<20}\")\n",
    "print(f\"{'提到二甲双胍':>30} {'是' if has_metformin else '否':<20} {'是' if has_metformin_mem else '否':<20}\")\n",
    "print(f\"{'使用记忆来源':>30} {'无':<20} {' + '.join(mem_sources) if mem_sources else '无':<20}\")\n",
    "print(f\"\\n结论：\")\n",
    "print(f\"  患者Prompt中没有写药名 → 药名信息只存在于记忆系统中\")\n",
    "print(f\"  无记忆时，患者无法说出具体药名（只能说'降压药''糖尿病的药'）\")\n",
    "print(f\"  有记忆时，患者能从长期记忆中检索到之前护士告知的药名\")\n",
    "print(f\"  这就是记忆模块的核心价值！\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## Part 3：记忆状态分析"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-17T16:57:42.225974Z",
     "iopub.status.busy": "2026-04-17T16:57:42.225771Z",
     "iopub.status.idle": "2026-04-17T16:57:42.248205Z",
     "shell.execute_reply": "2026-04-17T16:57:42.247645Z",
     "shell.execute_reply.started": "2026-04-17T16:57:42.225958Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "============================================================\n",
      "最终记忆状态\n",
      "============================================================\n",
      "\n",
      "【短期记忆（JSON文件）】\n",
      "短期记忆 (5/5):\n",
      "  1. [情绪] 你晚上睡得好吗？会不会想很多事情睡不着？ (访问2次)\n",
      "  2. [情绪] 你会不会有时候觉得一个人挺孤单的？ (访问1次)\n",
      "  3. [用药] 对了，你还记得之前我跟你说的那个降压药叫什么名字吗... (访问3次)\n",
      "  4. [用药] 那糖尿病的药呢？叫什么？一天吃几次来着？ (访问2次)\n",
      "  5. [用药] 你吃的所有药能帮我再说一遍吗？我做个确认。 (访问1次)\n",
      "\n",
      "【长期记忆（数据库）】\n",
      "长期记忆 (7条):\n",
      "  #34 [用药] 你好李文清，我是护士小王。我看了你的病历，你现在吃...\n",
      "  #35 [用药] 你还有一个糖尿病的药叫二甲双胍，一天两次，早晚各一...\n",
      "  #36 [用药] 好，那你现在吃的药能跟我说一遍吗？降压药和糖尿病药...\n",
      "  #37 [生活] 你平时在家一般做些什么？\n",
      "  #38 [生活] 有没有出去散步的习惯？\n",
      "  #39 [生活] 吃饭都是自己做吗？还是叫外卖？\n",
      "  #40 [家庭] 你家里人会经常来看你吗？\n"
     ]
    }
   ],
   "source": [
    "# 打印最终记忆状态\n",
    "print(\"=\" * 60)\n",
    "print(\"最终记忆状态\")\n",
    "print(\"=\" * 60)\n",
    "\n",
    "print(\"\\n【短期记忆（JSON文件）】\")\n",
    "memory.print_stm()\n",
    "\n",
    "print(\"\\n【长期记忆（数据库）】\")\n",
    "memory.print_ltm()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-17T16:57:44.315607Z",
     "iopub.status.busy": "2026-04-17T16:57:44.315407Z",
     "iopub.status.idle": "2026-04-17T16:57:44.321132Z",
     "shell.execute_reply": "2026-04-17T16:57:44.320621Z",
     "shell.execute_reply.started": "2026-04-17T16:57:44.315587Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "============================================================\n",
      "12轮记忆流转记录\n",
      "============================================================\n",
      "\n",
      "轮次    话题     相关性      存入位置               使用记忆         短期/长期\n",
      "----------------------------------------------------------------------\n",
      "1     用药     0.00     短期记忆               无            1/5|0\n",
      "2     用药     0.66     短期记忆               短期           2/5|0\n",
      "3     用药     0.67     短期记忆               短期           3/5|0\n",
      "4     生活     0.00     短期记忆               无            4/5|0\n",
      "5     生活     0.62     短期记忆               短期           5/5|0\n",
      "6     生活     0.60     短期记忆               短期           5/5|1\n",
      "7     情绪     0.00     短期记忆               无            5/5|2\n",
      "8     家庭     0.02     短期记忆               无            5/5|3\n",
      "9     情绪     0.64     短期记忆               短期           5/5|4\n",
      "10    用药     0.00     短期记忆               长期           5/5|5\n",
      "11    用药     0.60     短期记忆               短期+长期        5/5|6\n",
      "12    用药     0.60     短期记忆               短期+长期        5/5|7\n",
      "\n",
      "汇总:\n",
      "  存入短期记忆: 12次\n",
      "  存入长期记忆: 0次\n",
      "  使用了记忆回答: 8/12轮\n"
     ]
    }
   ],
   "source": [
    "# 记忆流转分析\n",
    "print(\"=\" * 60)\n",
    "print(\"12轮记忆流转记录\")\n",
    "print(\"=\" * 60)\n",
    "print(f\"\\n{'轮次':<5} {'话题':<6} {'相关性':<8} {'存入位置':<18} {'使用记忆':<12} {'短期/长期'}\")\n",
    "print(\"-\" * 70)\n",
    "\n",
    "for d in dialogue_log:\n",
    "    mem_used = \"\"\n",
    "    if d[\"used_stm\"] and d[\"used_ltm\"]:\n",
    "        mem_used = \"短期+长期\"\n",
    "    elif d[\"used_stm\"]:\n",
    "        mem_used = \"短期\"\n",
    "    elif d[\"used_ltm\"]:\n",
    "        mem_used = \"长期\"\n",
    "    else:\n",
    "        mem_used = \"无\"\n",
    "\n",
    "    print(f\"{d['round']:<5} {d['topic']:<6} {d['relevance']:<8.2f} \"\n",
    "          f\"{d['destination']:<18} {mem_used:<12} \"\n",
    "          f\"{d['stm_count']}/{memory.capacity}|{d['ltm_count']}\")\n",
    "\n",
    "# 统计\n",
    "stm_count = sum(1 for d in dialogue_log if \"短期\" in d[\"destination\"])\n",
    "ltm_count = sum(1 for d in dialogue_log if \"长期\" in d[\"destination\"])\n",
    "mem_used_count = sum(1 for d in dialogue_log if d[\"used_stm\"] or d[\"used_ltm\"])\n",
    "\n",
    "print(f\"\\n汇总:\")\n",
    "print(f\"  存入短期记忆: {stm_count}次\")\n",
    "print(f\"  存入长期记忆: {ltm_count}次\")\n",
    "print(f\"  使用了记忆回答: {mem_used_count}/{len(dialogue_log)}轮\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2026-04-17T16:58:14.231421Z",
     "iopub.status.busy": "2026-04-17T16:58:14.231251Z",
     "iopub.status.idle": "2026-04-17T16:58:14.258687Z",
     "shell.execute_reply": "2026-04-17T16:58:14.258128Z",
     "shell.execute_reply.started": "2026-04-17T16:58:14.231406Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "结果已保存到 memory_dialogue_result.txt\n"
     ]
    }
   ],
   "source": [
    "# 保存完整结果\n",
    "timestamp = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n",
    "\n",
    "with open(\"memory_dialogue_result.txt\", \"w\", encoding=\"utf-8\") as f:\n",
    "    f.write(\"=\" * 60 + \"\\n\")\n",
    "    f.write(\"记忆增强智能体 —— 12轮对话记录\\n\")\n",
    "    f.write(f\"时间: {timestamp}\\n\")\n",
    "    f.write(\"=\" * 60 + \"\\n\\n\")\n",
    "\n",
    "    for d in dialogue_log:\n",
    "        f.write(f\"--- 第 {d['round']} 轮 [{d['topic']}] ---\\n\")\n",
    "        f.write(f\"护士: {d['question']}\\n\")\n",
    "        f.write(f\"患者: {d['answer']}\\n\")\n",
    "        f.write(f\"相关性: {d['relevance']:.2f} → {d['destination']}\\n\")\n",
    "        mem_used = []\n",
    "        if d[\"used_stm\"]: mem_used.append(\"短期\")\n",
    "        if d[\"used_ltm\"]: mem_used.append(\"长期\")\n",
    "        f.write(f\"参考记忆: {'+'.join(mem_used) if mem_used else '无'}\\n\")\n",
    "        f.write(f\"记忆状态: 短期{d['stm_count']}/{memory.capacity}, 长期{d['ltm_count']}\\n\\n\")\n",
    "\n",
    "    # 写入记忆最终状态\n",
    "    f.write(\"=\" * 60 + \"\\n\")\n",
    "    f.write(\"【最终短期记忆内容】\\n\")\n",
    "    for key, mem in memory.short_term.items():\n",
    "        f.write(f\"  [{mem['topic']}] {mem['question'][:30]}... (访问{mem['access_count']}次)\\n\")\n",
    "\n",
    "    f.write(\"\\n【最终长期记忆内容】\\n\")\n",
    "    conn = sqlite3.connect(memory.db_path)\n",
    "    cursor = conn.execute(\"SELECT question, topic FROM long_term_memory\")\n",
    "    for r in cursor.fetchall():\n",
    "        f.write(f\"  [{r[1]}] {r[0][:30]}...\\n\")\n",
    "    conn.close()\n",
    "\n",
    "print(\"结果已保存到 memory_dialogue_result.txt\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 拓展：查看数据库内容（SQL查询）\n",
    "\n",
    "下面的代码展示如何直接查询长期记忆数据库。\n",
    "SQL语法与MySQL完全兼容——如果迁移到MySQL，这些查询不需要修改。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-17T16:58:16.791335Z",
     "iopub.status.busy": "2026-04-17T16:58:16.791166Z",
     "iopub.status.idle": "2026-04-17T16:58:16.816981Z",
     "shell.execute_reply": "2026-04-17T16:58:16.816455Z",
     "shell.execute_reply.started": "2026-04-17T16:58:16.791321Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 查询1: 所有长期记忆 ===\n",
      "  #34 [用药] Q: 你好李文清，我是护士小王。我看了你的病历，你现在吃的降压药是...\n",
      "       A: 我知道了，小王护士。（点头）我会尽量记得早上按时吃的。...\n",
      "\n",
      "  #35 [用药] Q: 你还有一个糖尿病的药叫二甲双胍，一天两次，早晚各一次。你记住...\n",
      "       A: 我记住了，二甲双胍一天两次，早晚各一次，谢谢小王护士！...\n",
      "\n",
      "  #36 [用药] Q: 好，那你现在吃的药能跟我说一遍吗？降压药和糖尿病药分别叫什么...\n",
      "       A: 降压药是氨氯地平，糖尿病药是二甲双胍。（轻轻点头）...\n",
      "\n",
      "  #37 [生活] Q: 你平时在家一般做些什么？...\n",
      "       A: 大多时候看看电视、读读报纸，天气好就去小区里散散步，偶尔收拾...\n",
      "\n",
      "  #38 [生活] Q: 有没有出去散步的习惯？...\n",
      "       A: 天气好的时候会去小区里散散步。（低头抠衣角）...\n",
      "\n",
      "  #39 [生活] Q: 吃饭都是自己做吗？还是叫外卖？...\n",
      "       A: 都是自己做，年纪大了吃不来外卖那味儿。（低头擦了擦衣角）...\n",
      "\n",
      "  #40 [家庭] Q: 你家里人会经常来看你吗？...\n",
      "       A: 我女儿每周来两次，忙的话会提前说。（抿抿嘴）老伴儿每天也来陪...\n",
      "\n",
      "=== 查询2: 按话题统计 ===\n",
      "  家庭: 1条\n",
      "  生活: 3条\n",
      "  用药: 3条\n",
      "\n",
      "=== 查询3: 搜索含'药'的记忆 ===\n",
      "  Q: 你好李文清，我是护士小王。我看了你的病历，你现在吃的降压药是氨氯地平，一天一次，知道吗？\n",
      "  A: 我知道了，小王护士。（点头）我会尽量记得早上按时吃的。\n",
      "\n",
      "  Q: 好，那你现在吃的药能跟我说一遍吗？降压药和糖尿病药分别叫什么？\n",
      "  A: 降压药是氨氯地平，糖尿病药是二甲双胍。（轻轻点头）\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# 直接查询SQLite数据库（SQL语法兼容MySQL）\n",
    "conn = sqlite3.connect(\"long_term_memory.db\")\n",
    "\n",
    "print(\"=== 查询1: 所有长期记忆 ===\")\n",
    "cursor = conn.execute(\"SELECT id, question, answer, topic, created_at FROM long_term_memory\")\n",
    "for row in cursor.fetchall():\n",
    "    print(f\"  #{row[0]} [{row[3]}] Q: {row[1][:30]}...\")\n",
    "    print(f\"       A: {row[2][:30]}...\")\n",
    "    print()\n",
    "\n",
    "print(\"=== 查询2: 按话题统计 ===\")\n",
    "cursor = conn.execute(\n",
    "    \"SELECT topic, COUNT(*) as cnt FROM long_term_memory GROUP BY topic\"\n",
    ")\n",
    "for row in cursor.fetchall():\n",
    "    print(f\"  {row[0]}: {row[1]}条\")\n",
    "\n",
    "print(\"\\n=== 查询3: 搜索含'药'的记忆 ===\")\n",
    "cursor = conn.execute(\n",
    "    \"SELECT question, answer FROM long_term_memory WHERE keywords LIKE '%药%'\"\n",
    ")\n",
    "for row in cursor.fetchall():\n",
    "    print(f\"  Q: {row[0]}\")\n",
    "    print(f\"  A: {row[1]}\")\n",
    "    print()\n",
    "\n",
    "conn.close()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-04-17T16:58:20.306279Z",
     "iopub.status.busy": "2026-04-17T16:58:20.306111Z",
     "iopub.status.idle": "2026-04-17T16:58:20.311665Z",
     "shell.execute_reply": "2026-04-17T16:58:20.311171Z",
     "shell.execute_reply.started": "2026-04-17T16:58:20.306265Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== 短期记忆文件内容 (short_term_memory.json) ===\n",
      "{\n",
      "  \"capacity\": 5,\n",
      "  \"count\": 5,\n",
      "  \"memories\": [\n",
      "    {\n",
      "      \"id\": \"1776444999623\",\n",
      "      \"question\": \"你晚上睡得好吗？会不会想很多事情睡不着？\",\n",
      "      \"answer\": \"（叹气）是啊，最近总睡不踏实，躺床上就忍不住琢磨白天的事，翻好久才能睡着。\",\n",
      "      \"keywords\": \"睡得,叹气,事情,最近,总睡,踏实,白天,忍不住,睡着,不会,才能,很多,晚上,床上,琢磨,睡不着,好久\",\n",
      "      \"topic\": \"情绪\",\n",
      "      \"timestamp\": 1776444999.623759,\n",
      "      \"access_count\": 2\n",
      "    },\n",
      "    {\n",
      "      \"id\": \"1776445011989\",\n",
      "      \"question\": \"你会不会有时候觉得一个人挺孤单的？\",\n",
      "      \"answer\": \"其实……有时候确实会有这种感觉，尤其是晚上躺在床上翻来覆去睡不着的时候，脑子里乱哄哄的，就更觉得孤单了。\",\n",
      "      \"keywords\": \"觉得,其实,这种,翻来覆去,时候,尤其,乱哄哄,不会,确实,晚上,床上,有时候,睡不着,孤单,脑子里,感觉\",\n",
      "      \"topic\": \"情绪\",\n",
      "      \"timestamp\": 1776445011.9890707,\n",
      "      \"access_count\": 1\n",
      "    },\n",
      "    {\n",
      "      \"id\": \"1776445018354\",\n",
      "      \"question\": \"对了，你还记得之前我跟你说的那个降压药叫什么名字吗？\",\n",
      "      \"answer\": \"氨氯地平……我记得的。（轻轻点头）\",\n",
      "      \"keywords\": \"降压药,名字,点头,氨氯地平,记得,轻轻,之前\",\n",
      "      \"topic\": \"用药\",\n",
      "      \"timestamp\": 1776445018.3546007,\n",
      "      \"access_count\": 3\n",
      "    },\n",
      "    {\n",
      "      \"id\": \"1776445023696\",\n",
      "      \"question\": \"那糖尿病的药呢？叫什么？一天吃几次来着？\",\n",
      "      \"answer\": \"是二甲双胍，一天两次，早晚各一次。（轻轻点头）\",\n",
      "      \"keywords\": \"糖尿病,几次,两次,点头,早晚,一次,二甲,轻轻,一天\",\n",
      "      \"topic\": \"用药\",\n",
      "      \"timestamp\": 1776445023.6968458,\n",
      "      \"access_count\": 2\n",
      "    },\n",
      "    {\n",
      "      \"id\": \"1776445029907\",\n",
      "      \"question\": \"你吃的所有药能帮我再说一遍吗？我做个确认。\",\n",
      "      \"answer\": \"降压药是氨氯地平，糖尿病的药是二甲双胍，一天两次，早晚各一次。（轻轻点头）\",\n",
      "      \"keywords\": \"糖尿病,降压药,药能,两次,再说,点头,早晚,氨氯地平,一遍,一次,所有,二甲,轻轻,一天,确认\",\n",
      "      \"topic\": \"用药\",\n",
      "      \"timestamp\": 1776445029.9075544,\n",
      "      \"access_count\": 1\n",
      "    }\n",
      "  ]\n",
      "}\n",
      "  ... (内容过长，已截断)\n"
     ]
    }
   ],
   "source": [
    "# 查看短期记忆JSON文件内容\n",
    "print(\"=== 短期记忆文件内容 (short_term_memory.json) ===\")\n",
    "with open(\"short_term_memory.json\", \"r\", encoding=\"utf-8\") as f:\n",
    "    stm_data = json.load(f)\n",
    "    print(json.dumps(stm_data, ensure_ascii=False, indent=2)[:2000])\n",
    "    if len(json.dumps(stm_data)) > 2000:\n",
    "        print(\"  ... (内容过长，已截断)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 拓展：MySQL连接（可选）\n",
    "\n",
    "如果你有MySQL服务器，只需替换数据库连接方式。\n",
    "SQL查询语句完全一样，不需要修改。\n",
    "\n",
    "```python\n",
    "# pip install pymysql\n",
    "import pymysql\n",
    "\n",
    "conn = pymysql.connect(\n",
    "    host=\"你的MySQL地址\",\n",
    "    port=3306,\n",
    "    user=\"用户名\",\n",
    "    password=\"密码\",\n",
    "    database=\"simulation\",\n",
    "    charset=\"utf8mb4\"\n",
    ")\n",
    "\n",
    "# 建表（MySQL版本，仅AUTO_INCREMENT不同）\n",
    "conn.cursor().execute(\"\"\"\n",
    "    CREATE TABLE IF NOT EXISTS long_term_memory (\n",
    "        id INT PRIMARY KEY AUTO_INCREMENT,\n",
    "        question TEXT NOT NULL,\n",
    "        answer TEXT NOT NULL,\n",
    "        keywords TEXT,\n",
    "        topic VARCHAR(50),\n",
    "        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n",
    "    )\n",
    "\"\"\")\n",
    "conn.commit()\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 拓展：交互式记忆对话\n",
    "\n",
    "手动输入问题与AI患者对话，观察记忆的实时变化。\n",
    "\n",
    "> 输入 `quit` 退出，输入 `memory` 查看当前记忆状态"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# 交互式记忆对话\n",
    "print(\"=\" * 50)\n",
    "print(\"交互式记忆对话\")\n",
    "print(\"输入 quit 退出 | 输入 memory 查看记忆状态\")\n",
    "print(\"=\" * 50)\n",
    "\n",
    "round_num = 0\n",
    "\n",
    "while True:\n",
    "    user_input = input(\"\\n护士: \").strip()\n",
    "\n",
    "    if user_input.lower() in [\"quit\", \"exit\", \"q\", \"退出\"]:\n",
    "        print(\"\\n对话结束。\")\n",
    "        break\n",
    "\n",
    "    if user_input.lower() == \"memory\":\n",
    "        print(\"\\n--- 当前记忆状态 ---\")\n",
    "        memory.print_stm()\n",
    "        print()\n",
    "        memory.print_ltm()\n",
    "        continue\n",
    "\n",
    "    if not user_input:\n",
    "        continue\n",
    "\n",
    "    round_num += 1\n",
    "\n",
    "    # 带记忆调用\n",
    "    answer, used_stm, used_ltm = call_patient_with_memory(user_input, memory)\n",
    "    print(f\"患者: {answer}\")\n",
    "\n",
    "    # 存入记忆\n",
    "    result = memory.process_new_memory(user_input, answer)\n",
    "    stats = memory.get_stats()\n",
    "    print(f\"  [{result['destination']}, 相关性={result['relevance']:.2f}, \"\n",
    "          f\"短期={stats['stm_count']}/{stats['stm_capacity']}, \"\n",
    "          f\"长期={stats['ltm_count']}]\")\n",
    "\n",
    "print(f\"\\n共进行了 {round_num} 轮对话\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 实操完成！\n",
    "\n",
    "### 生成的文件\n",
    "- `short_term_memory.json` —— 短期记忆（JSON文件，可直接查看）\n",
    "- `long_term_memory.db` —— 长期记忆（SQLite数据库，SQL兼容MySQL）\n",
    "- `memory_dialogue_result.txt` —— 12轮对话完整记录\n",
    "\n",
    "### 你学会了\n",
    "1. **LRU算法**：最近常用的留在手边，不常用的存进仓库\n",
    "2. **相关性计算**：jieba分词 + Jaccard相似度 + 时间衰减\n",
    "3. **两级存储**：短期(JSON) + 长期(数据库)\n",
    "4. **记忆增强对话**：检索记忆 → 拼接上下文 → 调用工作流\n",
    "\n",
    "### 课后尝试\n",
    "- 修改 `capacity=5`，观察更频繁的LRU淘汰\n",
    "- 修改 `relevance_threshold=0.3`，观察更多对话进入长期记忆\n",
    "- 连接MySQL替换SQLite（SQL语句不用改）"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
