
想象这样一个场景家里的长辈拿起手机像平常聊天一样随口说了一句“我年轻的时候在厂里当车工有一年评先进车间主任把我叫到办公室……”手机对面的 AI 不急着讲道理而是轻轻应了一句“那后来呢”等长辈把这段往事讲完AI 已经把其中的人名、年份、地点、情绪悄悄整理成了结构化记忆。攒上几十段这样的记忆碎片一本属于老人的回忆录就慢慢有了雏形。这篇文章将完整拆解背后这个 AI Agent 智能体开发过程也就是“给 AI 讲故事AI 帮你写成回忆录”。这里既有 Agent 的意图判断、结构化抽取、长期记忆设计也有语音输入、回忆录生成、接口封装等工程落地细节。如果你想做一个 AI 养老方向的智能体应用或者正在学 agent 开发这篇文章能给你一套可以直接跑起来的最小闭环。1. 背景与场景为什么“AI 陪聊 回忆录”适合用智能体来做1.1 养老场景的真实痛点老年人的精神陪伴需求长期被简单概括为“多陪陪老人”。但现实中子女工作忙、异地居住、生活节奏快真正能坐下来听老人讲完整段往事的机会并不多。更可惜的是老人的口头记忆一直在流失。很多老人年轻时经历的年代故事、家族迁徙、职业变迁、人生关键节点从来没有被系统记录过。等老人讲不动了这些故事也就永远消失了。传统方案里有人用录音笔把老人的讲述录下来有人拿笔记本随手记。但录音笔的问题在于录完就变成了难以检索的音频文件几小时的录音很难转成文字更别说整理成有结构的回忆录。手写笔记则更依赖记录者的耐心和整理能力普通人很难坚持。这正是 AI Agent 可以切入的地方。它不只做语音转文字还能理解老人的讲述、判断哪些内容是值得保存的人生片段、自动提取时间地点人物、追问遗漏细节最后把这些碎片拼成一本可阅读、可打印的回忆录。1.2 回忆录智能体要解决什么问题这个智能体本质上要完成三件事听接收老人的语音或文字输入让老人用最自然的方式讲述往事。记从对话中提取结构化记忆比如事件、时间、地点、人物、情感而不是简单保存聊天记录。写在用户要求时把所有记忆片段整理成有标题、有时间线、有人物关系的回忆录章节。举个例子。老人说“我记得是 1982 年我从黑龙江调到北京那时候我女儿刚上小学。”AI 要能识别出intent 是 storytelling老人正在讲述过往经历time_period 是“1982 年”location 是“黑龙江、北京”people 包含“女儿”episode 是“从黑龙江调到北京当时女儿刚上小学”。这个结构化的过程决定了后续回忆录能不能写清楚。如果只是把原话存进数据库生成时就会变成一堆杂乱文字的堆砌。1.3 技术定位它属于哪一类 AI 应用很多人会把这个项目简单理解成“聊天机器人”。但实际上它比聊天机器人要多两层能力长期记忆和内容生成。如果单纯用大模型 API 做多轮对话模型是无法记住上一轮讲过的事情的。上下文窗口再大关掉对话就丢失了。回忆录智能体必须把用户讲过的故事沉淀到自己的存储系统里下次生成时再去检索。这就进入了 AI Agent 的范畴。从技术组成看这个项目是多个能力的组合ASR自动语音识别把老人说的话转成文字LLM大语言模型理解意图、抽取信息、生成回复结构化输出让模型按固定 JSON 格式返回内容长期记忆把结构化的故事片段持久化内容生成根据记忆片段写回忆录章节。这种组合方式正好对应了当前 agent 开发中很常见的“感知-决策-执行”循环感知用户输入决策用户意图执行保存记忆或生成内容。2. Agent 智能体开发核心概念2.1 AI Agent 是什么AI Agent中文一般叫智能体核心特点是“由大模型做决策自主完成任务”。传统软件的逻辑是写死的。用户点哪个按钮程序就执行哪个函数。而 Agent 面对的是开放输入模型需要先判断“用户想让我干什么”再决定调用什么能力、按照什么顺序执行。回到这个项目。用户可能说“我年轻的时候在部队开过车。”讲故事“你好呀今天天气不错。”闲聊“帮我把刚才那些故事整理成回忆录。”发布指令这三句话如果交给固定规则去判断也能做但会很脆弱。用户换个说法规则就失灵了。用大模型做意图识别能覆盖几乎无限种表达方式。2.2 Agent、Workflow、RAG 的分工很多刚接触 agent 开发的读者会把这几个概念搞混这里简单区分一下Workflow固定流程。比如“先转写再抽取再入库”步骤是提前设计好的。Agent在大模型驱动下动态决策。它可以根据输入决定走哪条路甚至调用不同工具。RAG检索增强生成生成时先从外部知识库检索相关内容再交给大模型回答。回忆录智能体并不是纯 Agent而是“Workflow Agent RAG”的混合体。固定流程负责“转写→抽取→存储”这种确定性步骤Agent 负责判断“这次输入是讲故事还是下指令”生成回忆录时又会先从数据库检索这个用户的全部记忆片段再让大模型写正文这就是 RAG 的思想。2.3 记忆系统聊天记录不是记忆在设计智能体时一个常见误区是把“聊天记录”当成“记忆”。聊天记录是原始对话流里面混着大量无效信息需要检索的时候很难用。而记忆是经过理解、过滤、结构化之后的知识。举个例子聊天记录保存的是“我记得是 1982 年我从黑龙江调到北京那时候我女儿刚上小学。”结构化记忆保存的是time_period1982 年、location黑龙江→北京、people[女儿]、episode从黑龙江调到北京当时女儿刚上小学。生成回忆录时程序读的是结构化记忆而不是原始聊天记录。只有结构化之后才能按时间排序、按人物归类、按地点串联才能生成一本逻辑清晰的回忆录。所以在这个项目中记忆模块是核心中的核心。它决定了回忆录的下限。3. 环境准备与项目规划3.1 技术选型这个项目我用 Python 实现主要组件如下Python 3.10语言基础环境FastAPI提供 HTTP 接口方便前端页面调用openai SDK调用大模型接口因为很多大模型平台都提供 OpenAI 兼容接口这样代码不用绑定某一家厂商SQLite保存结构化记忆零配置、适合 Demo浏览器 Web Speech API实现语音输入不需要额外申请语音识别服务Ollama可选本地跑大模型方便调试和演示。模型方面本教程没有绑定具体厂商。你可以把接口地址改为任意兼容 OpenAI 格式的大模型服务也可以本地用 Ollama 跑一个 7B 以上的模型。不同模型对 JSON 输出的稳定性不同这一点后面会专门讲。3.2 项目目录结构整个项目按模块拆分方便后续扩展ai-story-agent/ ├── requirements.txt ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 入口 │ ├── config.py # 配置读取 │ ├── schemas.py # Pydantic 数据模型 │ ├── llm.py # 大模型调用与 JSON 解析 │ ├── memory.py # 记忆存储模块 │ ├── agent.py # Agent 编排核心逻辑 │ ├── story_writer.py # 回忆录生成模块 │ └── prompt_templates.py # Prompt 模板 ├── static/ │ └── index.html # 前端页面 └── data/ # SQLite 文件目录自动创建这种结构比较适合学习。每个文件职责单一能看到一条清晰的调用链前端请求 → FastAPI 接口 → Agent 编排 → 大模型调用 → 记忆存储。3.3 环境变量与依赖先创建虚拟环境并安装依赖python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install fastapi uvicorn openai pydantic为了不写死版本这里按“当前最新稳定版”安装即可。如果你使用云厂商的大模型服务需要额外确认 SDK 版本是否兼容 OpenAI 格式。将配置写入app/config.py通过环境变量覆盖方便切换模型服务# app/config.py import os # 大模型接口地址默认指向本地 Ollama 的 OpenAI 兼容地址 LLM_BASE_URL os.getenv(LLM_BASE_URL, http://localhost:11434/v1) LLM_API_KEY os.getenv(LLM_API_KEY, ollama) LLM_MODEL os.getenv(LLM_MODEL, qwen2.5:7b) # 是否使用 JSON Mode。部分模型不支持 response_format会自动降级 USE_JSON_MODE os.getenv(USE_JSON_MODE, true).lower() true # SQLite 数据库文件路径 DB_PATH os.getenv(DB_PATH, data/memory.db)关键思路开发时先用本地小模型验证交互流程成本为零验证无误后再切到更强的云端模型提升稳定性。4. 系统设计从讲故事到生成回忆录4.1 整体流程整个智能体的流程可以拆成六个步骤用户输入用户通过页面说话或打字内容是一段往事。意图识别大模型判断用户是在讲故事、寒暄还是在发布指令。结构化抽取如果是在讲故事模型抽取事件、时间、地点、人物、情感、标签。记忆保存把结构化内容写入 SQLite方便后续检索和排序。生成回复模型对用户说一句温暖的回应让老人愿意继续讲。指令执行用户要求生成回忆录时从数据库读取全部记忆调用大模型分章节生成正文。前五步每轮对话都会发生第六步只在用户主动发起时执行。4.2 数据结构设计记忆表的结构如下字段类型说明idTEXT记忆唯一 IDuser_idTEXT用户标识支持多老人使用episodeTEXT这段故事的核心内容time_periodTEXT时间段描述如“1982 年”locationTEXT地点peopleTEXT相关人物列表JSON 数组emotionTEXT情感倾向tagsTEXT标签列表JSON 数组raw_textTEXT用户原始讲述用于核对created_atTEXT保存时间raw_text字段很多人会忽略但它在生产环境非常重要。生成回忆录时如果模型编造了内容可以通过raw_text做人工核对确认哪些细节是真实存在的。4.3 Prompt 设计要点Prompt 是这个项目的灵魂。我们至少需要三类 Prompt系统角色 Prompt设定 AI 是“记忆陪伴智能体”要倾听、共情、不评价、不打断。抽取 Prompt要求模型输出严格 JSON包含 intent、reply、memory 字段。写作 Prompt要求回忆录保持真实、第一人称、不编造。抽取 Prompt 的核心是给模型一个明确的 JSON 结构。模型只有知道每个字段的含义才能做出高质量抽取。5. 核心代码实战AI 讲故事智能体落地5.1 数据模型定义先定义接口层的数据结构# app/schemas.py from typing import Optional, List from pydantic import BaseModel, Field class ChatRequest(BaseModel): user_id: str Field(defaultdefault_user, description用户/老人标识) message: str Field(..., min_length1, description本次输入内容) session_id: Optional[str] Field(defaultNone, description会话 ID) class MemoryItem(BaseModel): memory_id: str user_id: str episode: str time_period: str location: str people: List[str] [] emotion: str tags: List[str] [] raw_text: str created_at: str class BookRequest(BaseModel): user_id: str title: str Field(default我的回忆录, description回忆录标题)Pydantic 在这里的作用是校验请求参数。比如message不能为空否则接口直接返回 422 错误避免空数据进入 Agent 流程。5.2 大模型调用与 JSON 解析大模型调用单独放在app/llm.py。这样 agent 和回忆录生成模块都能复用。# app/llm.py import json import re from openai import OpenAI from config import LLM_BASE_URL, LLM_API_KEY, LLM_MODEL, USE_JSON_MODE client OpenAI(base_urlLLM_BASE_URL, api_keyLLM_API_KEY) def chat(messages, temperature0.3): 调用大模型自动处理 json_mode 降级 kwargs { model: LLM_MODEL, messages: messages, temperature: temperature, } if USE_JSON_MODE: kwargs[response_format] {type: json_object} try: resp client.chat.completions.create(**kwargs) except Exception: # 模型不支持 response_format 时去掉该参数重试 kwargs.pop(response_format, None) resp client.chat.completions.create(**kwargs) return resp.choices[0].message.content def safe_parse_json(text: str) - dict: 尽可能从模型输出中解析出 JSON避免结构化输出失败导致流程中断 if not text: return {intent: chat, reply: 我在听呢你慢慢说。, memory: None} text text.strip() # 1. 直接解析 try: data json.loads(text) if isinstance(data, dict): return data except json.JSONDecodeError: pass # 2. 匹配 json ... 代码块 match re.search(r(?:json)?\s*([\s\S]*?), text) if match: try: data json.loads(match.group(1).strip()) if isinstance(data, dict): return data except json.JSONDecodeError: pass # 3. 截取第一个 { 到最后一个 } 之间的内容 start, end text.find({), text.rfind(}) if start ! -1 and end ! -1 and end start: try: data json.loads(text[start:end 1]) if isinstance(data, dict): return data except json.JSONDecodeError: pass # 4. 兜底当作普通聊天 return {intent: chat, reply: text, memory: None}这里safe_parse_json是保证系统健壮性的关键。真实环境下模型输出不总是合法 JSON尤其本地小模型更容易翻车。我们要把“解析失败导致整个请求报错”的概率降到最低。5.3 记忆存储模块记忆模块使用 SQLite。每个请求创建独立连接写入后立即关闭避免长连接带来的多线程问题。# app/memory.py import json import sqlite3 import uuid from datetime import datetime, timezone from config import DB_PATH def get_conn(): conn sqlite3.connect(DB_PATH, timeout10) conn.row_factory sqlite3.Row return conn def init_db(): conn get_conn() try: conn.execute( CREATE TABLE IF NOT EXISTS memories ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, episode TEXT NOT NULL, time_period TEXT, location TEXT, people TEXT, emotion TEXT, tags TEXT, raw_text TEXT, created_at TEXT ) ) conn.execute( CREATE INDEX IF NOT EXISTS idx_user_time ON memories(user_id, created_at) ) conn.commit() finally: conn.close() def save_memory(user_id, memory, raw_text): memory_id str(uuid.uuid4()) created_at datetime.now(timezone.utc).isoformat() conn get_conn() try: conn.execute( INSERT INTO memories (id, user_id, episode, time_period, location, people, emotion, tags, raw_text, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) , ( memory_id, user_id, memory.get(episode, ), memory.get(time_period, ), memory.get(location, ), json.dumps(memory.get(people, []), ensure_asciiFalse), memory.get(emotion, ), json.dumps(memory.get(tags, []), ensure_asciiFalse), raw_text, created_at, ), ) conn.commit() return memory_id finally: conn.close() def list_memories(user_id): conn get_conn() try: rows conn.execute( SELECT * FROM memories WHERE user_id ? ORDER BY created_at ASC, (user_id,), ).fetchall() return [dict(row) for row in rows] finally: conn.close() def delete_memory(user_id, memory_id): conn get_conn() try: conn.execute( DELETE FROM memories WHERE user_id ? AND id ?, (user_id, memory_id), ) conn.commit() finally: conn.close()存储时用json.dumps把人物和标签列表变成 JSON 字符串。取出来使用时再json.loads还原。SQLite 没有数组类型这是常见的做法。5.4 Agent 编排核心逻辑Agent 编排是核心。这里定义两类 Prompt并实现run_agent函数。# app/prompt_templates.py SYSTEM_PROMPT 你是一位耐心的记忆陪伴智能体正在陪伴一位长辈聊天。 你的任务是倾听长辈讲述的往事用温和的语气回应并从对话中保存值得记录的人生片段。 要求 1. 不要编造长辈没有说过的信息。 2. 回应要简短自然像晚辈在听长辈讲故事不要说教不要像客服。 3. 当对方明确要求生成或整理回忆录时将 intent 设为 command。 EXTRACT_PROMPT 请分析用户的输入输出严格 JSON格式如下 { intent: storytelling 或 chat 或 command, reply: 你作为倾听者对用户说的回应语简短自然, memory: null } 如果 intent 是 storytelling则 memory 必须包含 { episode: 这段故事的核心内容保留关键细节, time_period: 时间段如 1982 年用户没说就写不详, location: 地点用户没说就写不详, people: [人物1, 人物2], emotion: 整体情感倾向, tags: [标签1, 标签2] } 规则 - storytelling 表示用户在讲述过往经历。 - chat 表示普通寒暄比如你好、在吗、今天天气不错。 - command 表示用户要求生成回忆录、整理故事、写成一本书等。 - 只提取用户明确提到的信息绝不编造。 用户输入 {message} WRITER_PROMPT 你是回忆录写手。根据提供的真实素材写回忆录的一章。 要求 1. 使用长辈第一人称口吻语言朴实自然。 2. 按素材中的时间顺序组织内容。 3. 只使用素材中出现的信息绝不补充细节。 4. 每一章给一个小标题。 5. 直接输出正文不要输出任何解释说明。 素材如下 {materials} 下面是 agent 编排模块# app/agent.py from config import USE_JSON_MODE from llm import chat, safe_parse_json from memory import save_memory from prompt_templates import SYSTEM_PROMPT, EXTRACT_PROMPT def run_agent(user_id, message): user_prompt EXTRACT_PROMPT.format(messagemessage) raw chat( [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_prompt}, ], temperature0.3, ) data safe_parse_json(raw) intent data.get(intent, chat) reply data.get(reply, 我在听你慢慢说。) memory data.get(memory) memory_saved False if intent storytelling and isinstance(memory, dict) and memory.get(episode): save_memory(user_id, memory, raw_textmessage) memory_saved True return { intent: intent, reply: reply, memory_saved: memory_saved, memory: memory, }可以看到真正的业务逻辑并不复杂调用模型解析 JSON判断意图保存记忆返回结果。这就是 Agent 的一种简单落地形态。5.5 回忆录生成模块生成回忆录时要考虑长文本问题。如果用户已经存了几百段故事一次性全部丢给大模型很可能超出上下文窗口。所以这里按每 6 个记忆片段生成一章最后拼接成完整回忆录。# app/story_writer.py import json from llm import chat from memory import list_memories from prompt_templates import SYSTEM_PROMPT, WRITER_PROMPT CHAPTER_SIZE 6 def _format_materials(memories): lines [] for index, item in enumerate(memories, 1): try: people 、.join(json.loads(item.get(people) or [])) except json.JSONDecodeError: people 不详 lines.append( f片段{index}时间{item.get(time_period, 不详)} f地点{item.get(location, 不详)} f人物{people} f内容{item.get(episode, )} ) return \n.join(lines) def _generate_chapter(memories, chapter_no): materials _format_materials(memories) prompt WRITER_PROMPT.format(materialsmaterials) text chat( [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: prompt}, ], temperature0.7, ) return f## 第 {chapter_no} 章\n\n{text.strip()} def generate_book(user_id, title我的回忆录): memories list_memories(user_id) if not memories: return 暂时还没有足够的故事素材请先让长辈讲几段往事。 chapters [] for i in range(0, len(memories), CHAPTER_SIZE): chunk memories[i:i CHAPTER_SIZE] chapter _generate_chapter(chunk, i // CHAPTER_SIZE 1) chapters.append(chapter) return f# {title}\n\n \n\n.join(chapters)生成时temperature0.7是为了让文字更自然。在抽取环节用0.3是为了让模型更稳定、更少发挥。5.6 FastAPI 接口与前端页面最后把所有模块串起来。# app/main.py from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from agent import run_agent from memory import delete_memory, init_db, list_memories from schemas import BookRequest, ChatRequest from story_writer import generate_book app FastAPI(titleAI 回忆录智能体) app.add_middleware( CORSMiddleware, allow_origins[*], allow_methods[*], allow_headers[*], ) app.on_event(startup) def startup(): init_db() app.get(/) def index(): return FileResponse(static/index.html) app.post(/api/chat) def chat(req: ChatRequest): return run_agent(req.user_id, req.message) app.get(/api/memories) def memories(user_id: str default_user): return list_memories(user_id) app.post(/api/book) def book(req: BookRequest): content generate_book(req.user_id, req.title) return {user_id: req.user_id, title: req.title, content: content} app.delete(/api/memories/{memory_id}) def remove_memory(memory_id: str, user_id: str default_user): delete_memory(user_id, memory_id) return {deleted: True}前端页面可以做一个极简版本。这里使用浏览器自带的 Web Speech API 做语音识别Chrome 系浏览器支持较好而且不需要申请额外的语音服务 key。!-- static/index.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleAI 回忆录智能体/title style body { font-family: sans-serif; max-width: 760px; margin: 40px auto; padding: 0 16px; } h1 { font-size: 22px; } #chatBox { border: 1px solid #ddd; border-radius: 8px; height: 320px; overflow-y: auto; padding: 12px; background: #fafafa; } #chatBox div { margin: 8px 0; } .user { text-align: right; color: #1a73e8; } .ai { color: #333; } .controls { display: flex; gap: 8px; margin-top: 12px; } input { flex: 1; padding: 10px; font-size: 16px; border: 1px solid #ddd; border-radius: 6px; } button { padding: 10px 16px; font-size: 15px; border: none; border-radius: 6px; background: #1a73e8; color: #fff; cursor: pointer; } /style /head body h1给 AI 讲故事AI 帮你写回忆录/h1 div idchatBox/div div classcontrols input idmsg placeholder讲一段往事或点击“说话”按钮 / button idsendBtn发送/button button idvoiceBtn说话/button button idbookBtn生成回忆录/button /div script const userId demo_user; const chatBox document.getElementById(chatBox); const msgInput document.getElementById(msg); function appendMessage(role, text) { const div document.createElement(div); div.className role; div.textContent text; chatBox.appendChild(div); chatBox.scrollTop chatBox.scrollHeight; } async function sendChat(text) { if (!text.trim()) return; appendMessage(user, text); msgInput.value ; const res await fetch(/api/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ user_id: userId, message: text }) }); const data await res.json(); appendMessage(ai, data.reply); if (data.memory_saved) { appendMessage(ai, 这段故事我已经记下来了); } } document.getElementById(sendBtn).onclick () sendChat(msgInput.value); document.getElementById(bookBtn).onclick async () { appendMessage(ai, 好的我来试着把之前的故事整理成回忆录……); const res await fetch(/api/book, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ user_id: userId, title: 我的回忆录 }) }); const data await res.json(); chatBox.innerHTML ; appendMessage(ai, data.content.replace(/\n/g, \n)); }; const SR window.SpeechRecognition || window.webkitSpeechRecognition; if (SR) { const recognition new SR(); recognition.lang zh-CN; recognition.continuous false; recognition.interimResults false; recognition.onresult (event) { const text event.results[0][0].transcript; sendChat(text); }; recognition.onerror (event) { appendMessage(ai, 语音识别失败 event.error); }; document.getElementById(voiceBtn).onclick () recognition.start(); } else { document.getElementById(voiceBtn).disabled true; document.getElementById(voiceBtn).textContent 浏览器不支持语音; } document.getElementById(msg).addEventListener(keydown, (e) { if (e.key Enter) sendChat(msgInput.value); }); /script /body /html这个页面追求的是“能跑、好理解”没有引入前端框架。生产环境完全可以用 Vue、React 重写但逻辑是相同的。6. 运行与验证6.1 启动服务在项目根目录执行uvicorn app.main:app --reload --port 8000如果使用本地 Ollama 模型需要确保 Ollama 已启动并且已经拉取对应模型ollama pull qwen2.5:7b然后打开浏览器访问http://127.0.0.1:8000/可以看到前端页面。6.2 用 curl 测试接口模拟用户讲述往事curl -X POST http://127.0.0.1:8000/api/chat \ -H Content-Type: application/json \ -d { user_id: demo_user, message: 我记得1982年我从黑龙江调到北京那时候我女儿刚上小学。 }正常情况下返回内容类似{ intent: storytelling, reply: 那后来呢女儿在北京适应得怎么样, memory_saved: true, memory: { episode: 1982年从黑龙江调到北京当时女儿刚上小学, time_period: 1982年, location: 黑龙江北京, people: [女儿], emotion: 怀念, tags: [工作调动, 家庭] } }此时数据库data/memory.db中已经写入一条记忆。继续讲几段故事后调用生成回忆录接口curl -X POST http://127.0.0.1:8000/api/book \ -H Content-Type: application/json \ -d {user_id: demo_user, title: 我的回忆录}返回的内容是按章节组织的 Markdown 文本。6.3 预期输出与观察点运行成功后重点观察几个环节对话中讲故事和普通寒暄是否能被正确区分抽取出的 time_period、location、people 是否符合预期模型回复是否自然、有没有说教感生成回忆录时是否按照时间顺序组织内容是否出现模型编造细节的情况。如果某个环节不稳定不要急着改代码先看是不是模型能力不够。小模型在结构化抽取上确实比大模型弱很多这是正常现象。7. 常见问题与排查思路问题现象常见原因解决思路模型返回内容无法解析成 JSON小模型对 JSON 格式理解能力弱换 7B 以上模型或关闭 json_mode 依赖兜底解析接口报错 400大模型服务不支持response_format参数代码已自动降级检查是否还有其它参数不兼容故事情节被模型编造Prompt 约束不够强或模型自由度太高降低 temperature在 Prompt 中强调“只能使用素材”SQLite 报 database is locked多线程并发写同一个 SQLite 文件设置 timeout10生产环境换 PostgreSQL/MySQL语音按钮不可用浏览器不支持 Web Speech API换 Chrome/Edge或接入服务端 ASR 方案生成内容超出上下文长度记忆片段太多一次性提交使用分章节生成每章限制素材数量老人说的话口语化太强抽取效果差模型对口语理解能力不足先用大模型做口语转书面再做结构化抽取这里重点说两个高频问题。第一个是“小模型经常输出非法 JSON”。这几乎无法通过代码完全规避只能从三个方向解决更换更强的大模型在抽取前增加一段“把口语转成简洁文本”的预处理步骤对关键业务做校验JSON 解析失败时不要把错误暴露给用户而是返回一句兜底回复。第二个是“模型生成回忆录时编造细节”。自动写作天然有幻觉风险处理方式不是盲目相信模型输出而是在写作 Prompt 中明确“只使用素材中出现的信息”生成后允许人工编辑把每段原始讲述raw_text一并展示方便核对。8. 最佳实践与工程建议8.1 隐私与数据安全养老场景的数据比普通互联网业务更敏感。这里有几条底线明确告知在页面和语音开场中告诉老人“你讲的内容会被记录”不能偷偷采集。最小权限系统只收集生成回忆录所需的信息不采集无关隐私。可删除提供删除单条记忆的接口老人或家属可以随时清除。权限隔离不同老人之间通过user_id完全隔离不允许跨用户访问。加密存储生产环境对数据库做加密备份文件同样要加密。人工审核涉及大模型生成内容建议增加“人工确认后再发布/打印”的环节。8.2 老年用户交互体验优化这个项目的用户不是年轻程序员而是可能不太会用智能手机的老人。交互设计要尽量简单语音优先能用语音就不让打字按键要大反馈要快。温暖反馈AI 回复不要只是“好的已记录”可以多问一句“那后来呢”引导老人继续讲。错误容忍老人说得断断续续、逻辑跳跃是常态模型不需要输出标准答案要能理解和承接。自动追问当老人提到一个模糊信息比如“厂里”、“老领导”可以在回复中自然地问“当时厂里叫什么名字呀”。这些交互细节决定了老人愿不愿意长期使用。技术只是前提。8.3 回忆录内容质量与幻觉控制生成回忆录时建议采用“回忆片段 章节生成 人工校对”的流程先让老人用短对话讲出多个故事片段每隔一段时间自动汇总成章生成后由老人或家属人工确认把明显错误的部分删掉或改掉最后统一排版打印。不建议在第一天就要求生成完整书籍。回忆录是长周期内容内容质量比生成速度更重要。8.4 生产环境落地建议从 Demo 到生产环境还需要做几件事把 SQLite 替换为 PostgreSQL 或 MySQL支持并发访问用向量数据库保存记忆按语义检索最相关的故事片段接入生产级 ASR 服务替代浏览器语音识别生成回忆录用异步任务队列避免长文本生成阻塞 HTTP 请求增加管理后台让运营人员能够查看生成记录、处理异常记录模型调用日志和 token 消耗做成本监控对模型输出做敏感词过滤和格式校验。如果不想从零开发流程编排也可以参考 Dify、Coze 等智能体平台先跑通产品逻辑再用代码自研实现更复杂的定制需求。Java 技术栈的开发者还可以参考 LangChain4j、Spring AI 等框架把这里的模块等价迁移过去。这个项目的核心价值不在于技术多复杂而在于把“倾听、记录、整理、成书”完整串起来。回到开头那个场景老人讲一段往事AI 认真听、仔细记、最后生成一本可以打印的家史这件事本身就是 AI 养老方向里很有温度的一次落地。建议你先把最小闭环跑起来用两三段真实的故事验证流程再逐步扩展章节生成、人物关系梳理、时间线可视化等功能。当长辈真的看到自己讲过的故事变成文字时你会明白这个智能体存在的意义。