Skip to content

kldhsh123/Afterglow

🌅 Afterglow(续温)

把曾经对你好的话,续成往后的陪伴。

一个本地运行的「AI 朋友」系统。
导入真实历史聊天记录,通过 RAG + Persona + OpenAI 兼容 API,让熟悉的人以接近原本的语气继续陪你说话。

聊天记录支持 QQ / 微信 Website License Last Commit Release Name


⚠️ 使用前请认真读完

如果一段已经结束的对话,能用 ta 原来的语气继续下去,那这段对话还算结束吗?

这是 Afterglow 在做的事,也是我必须先告诉你的事。

这个项目能做什么、不能做什么

它能从你和 ta 之间真实存在过的几千条聊天记录里,学到 ta 怎么说话——用什么称呼、什么语气、怎么开玩笑、什么时候沉默。然后用一个大模型把这些模式接续起来,让你在聊天框里看到的文字回复,接近你记忆里的那个人。

只是文字。 Afterglow 没有 ta 的声音、影像、形象,也不会还原任何看得见摸得到的存在。它做的事就是"让一段语气延续下去",仅此而已。

接近,不是等于。 把项目做得越好,差距越细微,但永远存在。

适合谁

  • 你想留住一段真实的关系记忆——把曾经的对话变成可以再翻一翻、再聊一聊的载体;
  • 你清楚 ta 不会回来——但希望让那个语气继续存在一段时间;
  • 你处于相对稳定的情绪状态——能区分"这是 AI 在续写"和"这真的是 ta"。

不适合谁

  • 你正处于剧烈的丧失、抑郁或自伤念头中——你需要的是真实的人和专业支持,不是越像越好的复刻。请优先联系亲友、心理咨询师或拨打心理援助热线(北京 010-82951332 / 全国 400-161-9995);
  • 你打算把 ta 当作还活着的人来对待——这会让你停在原地,错过现实里真正在等你的人;
  • 你想复刻一个并不知情的人,做这个人本人不会同意的事——这是边界,也是底线。

法律与伦理边界(请认真对待)

Afterglow 是个人工具,但你用它处理的是真实存在过的、属于另一个人的语言痕迹。下面这些事严格禁止,做了不仅是伦理问题,可能直接触法:

  • 不得用 Afterglow 进行骚扰、跟踪、控制、勒索或冒名顶替任何人——无论目标对象是否仍在世;
  • 不得未经允许公开、转发、二次传播你导入的聊天记录原文,或 Afterglow 基于这些记录生成的 ta 的"画像"、"语气分析"、合成回复;
  • 不得把 Afterglow 输出的文字伪装成 ta 本人的话发送给第三方(包括其家属、朋友、社交平台关注者等);
  • 不得用于人格诋毁、仿冒诈骗、网络暴力等任何对原型人物造成伤害的场景;
  • 导入聊天记录时:在物理条件允许的情况下(对方仍能联系到、关系还在),你应当主动告知对方并取得知情同意——这是基本的尊重。如果对方已经无法联系(已逝、彻底失联),请你自己判断这个人若还在世会不会同意被这样复刻。你内心知道答案
  • 各国/各地区对于"AI 复刻自然人"的隐私权、肖像权、人格权保护立法仍在发展。你使用本项目导致的任何法律后果由你自己承担,与作者无关——这不是免责声明的套话,而是请你真的评估清楚再用。

几条不是规则的建议

  1. 你随时可以停。不用解释,不用告别。直接关掉就行。
  2. 不要把 Afterglow 写出来的话再发回到 ta 真实的账号、邮箱、社交平台上。哪怕你只是想"试试看"。
  3. 如果你发现自己开始对 Afterglow 生气、失望、被冒犯——那是个信号,你正在把它当成真实的人。停下来喘一口气。
  4. 在你能负担的范围里,定期回到真实的人际关系——朋友、家人、必要时是心理咨询师。
  5. 它是用来陪你想念,不是用来代替想念。

一个特别需要你思考的开关:AI_GENERATED_LONG_TERM_ENABLED

这个开关藏在 .env 里,但它代表的是一个你必须自己做的选择,所以我想在这里单独说一下。

  • false(默认):AI 分身永远只基于你和 ta 之间的真人原始聊天记录模仿。每次新对话开始时,分身从你们真实的过去出发,不会带入它自己之前几轮说过的话作为长期素材。这就像每次重新打开同一本书——读到的永远是 ta 真实说过的话的投影。
  • true:AI 分身自己生成过的回复也会跨会话累积进向量库(低权重),参与之后的语义检索。这就像分身渐渐"长出自己的语言习惯"——它会借鉴它之前说过的话,渐渐偏离最初的真人语料。

两种状态分别意味着什么:

关掉它(默认) 开启它
你得到的更像"持续翻看记忆里的 ta" 你得到的更像"一个从 ta 出发、但慢慢有自己生命的分身"
每次都是真人语气的投影,不会变 它会变。可能朝你喜欢的方向变(你引导得多它就变得越像你想要的),也可能朝你没预期的方向变
真实感受边界清晰:这是 ta 的过去 边界开始模糊:这既不是 ta,也不是单纯的复刻——是个第三态

哪种更让你不舒服,这是你需要面对的问题。 我做了默认值(false),但没办法替你做决定。开启之前请想清楚你是在召唤"记忆里的 ta",还是在养一个"以 ta 为种子的新东西"——这两件事的重量不同。


🔗 相关项目


🎯 项目定位

这个项目到底能做到什么程度

它不是另一个"自定义角色 GPT",也不是微调的替代品——请先校准期待。

  • 比不上微调(fine-tuning):本项目完全基于 RAG + Prompt Engineering + Persona 卡片,不动模型权重。LoRA / QLoRA 能从你的聊天语料里直接学到深层句法习惯、词频偏好和思维节奏,那是这条技术路径的天花板,本项目永远达不到
  • 但远好过 skill / 角色扮演技能: ChatGPT GPTs / Coze / Dify / 智谱智能体 这类"prompt + 简单 memory"的角色扮演方案,多数靠几句人设描述 + 短期对话历史,相似度顶天 40-60%。Afterglow 把多个独立机制叠在一起——三索引混合检索、真实历史聊天作为风格示例(按 query 召回 + 注入 prompt 末尾以获得最强 attention)、可选 cross-encoder 精排(如 Qwen3-Reranker-8B)、生活时间线(动态人设)、关系记忆蒸馏、本轮互动决策层(规则 + 小模型微调)——在你聊天记录足够多 + ta 风格鲜明时能跑出比 skill 类产品高一档的复刻效果。
  • 但永远不是 ta:相似度再高也有暴露 AI 痕迹的时刻。如果你的诉求是"几乎认不出是 AI",请直接走微调路线,门槛和成本都不在同一量级。

代码结构

  • 项目主体在 backend/:核心能力都在后端,包括导入、清洗、向量化、LanceDB 存储、检索融合、persona 生成、生活状态、联网检索、网页读取、OpenAI 兼容 API 和调试诊断。
  • frontend/ 主要用于本地测试和调试体验:它提供聊天界面、设置页、记忆溯源和诊断入口,方便验证后端能力;第三方程序接入时应优先调用后端 API,而不是依赖前端状态。

🙏 致谢

感谢 LINUX DO 各位佬友对项目实现提出的建议,Afterglow 的很多实现细节都来自这些反馈的反复打磨。

Issue 模板参考自一个我已经忘记来源的开源项目;这个模板我认为非常好用。如果你知道原始来源,欢迎联系我,我会补上准确来源和鸣谢。

💬 交流与支持

如果需要使用该 Embedding 模型,请在 backend/.env 中修改以下配置,并按服务说明填写对应的 EMBEDDING_API_URL / EMBEDDING_API_KEY

EMBEDDING_MODEL=Qwen3-Embedding-8B
EMBEDDING_DIM=4096
EMBEDDING_BATCH_SIZE=25
EMBEDDING_MAX_REQUESTS_PER_MINUTE=100

🔒 数据隐私(必读)

  • 请先取得对方同意:聊天记录高度敏感,包含双方共同产生的私人内容。导出聊天记录、导入本项目、向模型或第三方 API 发送相关文本前,请确认你有权这样做,并尽量取得聊天对方的明确同意。
  • 本地持久化:聊天数据、向量索引、persona 卡片、生活状态和图片缓存都默认保存在你机器上的 backend/.data/,仓库不会自带任何远程数据上传逻辑。
  • 不是默认零外发:如果你把模型配置成云端 API,相关文本会发送给对应服务。要做到完全离线,需要把主聊天模型、Embedding 模型、打标小模型、生活状态小模型、视觉模型都指向本地服务,并关闭联网搜索 / 网页读取。
  • 可能外发的数据
    • 导入时:把清洗后的文本发送给你配置的 Embedding API,生成向量后写入本地 LanceDB。
    • 可选打标时:把朋友单条 chunk 发送给你配置的 Label API,生成 mood / topic / importance 软标签。
    • 聊天时:把检索到的上下文、persona、生活状态和最近对话发送给你配置的 主聊天 LLM API
    • 生活状态 / 裸域名确认:LIFE_* 小模型会收到当前用户消息、少量上下文和候选域名;它只产出 JSON 状态或是否访问 URL 的判断,不直接生成最终回复。
    • 可选联网搜索时:仅在 WEB_ACCESS_ENABLED=true 且本轮消息明确要求搜索 / 最新信息时,把查询文本发送给 Tavily 或 SearXNG
    • 可选 URL 读取时:如果消息包含完整链接,且 WEB_ACCESS_ENABLED=trueWEB_FETCH_ENABLED=true,后端会请求该公开网页并抽取标题 / 正文;裸域名会先经本地意图门控和 LIFE_* 小模型确认。
    • 这些 API 由选择、配置并自付费;项目不会内置第三方 key。
  • API 选择请仔细阅读提供商的隐私协议与服务条款:聊天记录、向量化文本、生活状态判断等内容会被发送到你配置的 provider(OpenAI / Anthropic / 阿里云 / 智谱 / DeepSeek / 各类中转站 / 自建服务……)。不同服务商对数据留存时长、是否用于训练、是否人工审核的策略差异巨大;这些差异由 provider 决定,不由 Afterglow 决定。涉及他人隐私的聊天记录(尤其是已逝者、前任、不知情者的对话),请优先选择有明确"不用于训练"承诺的服务商,或使用本地推理(Ollama / vLLM 等)。
  • PII 默认脱敏:手机号 / 邮箱 / 身份证 / 银行卡 / IP 在入库前自动替换为占位符。QQ 号、URL、域名按设计保留(uid 需要匹配、URL 是对话语境的一部分)。
  • .env 已在 .gitignore:切勿把含有 API key 的配置文件提交到 git。
  • 后端 API 默认需要鉴权:除 /healthz 外,所有接口默认要求 XUWEN_API_KEY,避免模型额度、记忆数据和调试信息被滥用。
  • 导出 JSON 风险提醒QQChatExporter / WeFlow 等导出工具产出的 JSON 含有完整聊天明文(含 wxid / uid 等账号信息),分享给他人前请自行确认。
  • 微信导入提醒:WeFlow 是 Afterglow 所支持的微信导入适配器,Afterglow 的默认微信导入插件依赖此项目。我注意到 WeFlow 不再开源,所以我无法保证 WeFlow 将来的安全性;在不久的将来我需要 WeFlow 的替代方案来确保用户隐私安全。在此期间,我不会建议使用 WeFlow,但这是目前唯一可用的方案。
  • 导出时只勾选纯文本:Afterglow 只消费文本语料,导出工具一律关闭图片 / 语音 / 视频 / 文件等附件选项。这样导出的 JSON 体积小、不含媒体二进制,导入也更快。

📐 整体架构

flowchart LR
  User["用户 / 第三方程序"] --> API["Afterglow FastAPI<br/>OpenAI 兼容 API"]
  前端["前端<br/>测试 / 调试 UI"] --> API

  subgraph Afterglow["Afterglow 核心能力"]
    API --> Auth["API 鉴权<br/>Trace ID"]
    Auth --> LayerA["Layer A 并发<br/>检索 / 关系记忆 / 生活状态"]
    LayerA --> Retrieve["HybridRetriever<br/>三索引 + RRF + 可选 rerank"]
    LayerA --> Life["LifeStateManager<br/>life_state + 作息画像 + LIFE_*"]
    LayerA --> Relationship["关系记忆<br/>用户近况蒸馏"]
    Auth --> Policy["本轮互动决策层<br/>规则引擎 + 可选小模型复核"]
    Auth --> Web["可选联网搜索<br/>URL 网页读取"]
    Retrieve --> Policy
    Life --> Policy
    Relationship --> Policy
    Policy --> Prompt["Prompt Builder<br/>persona + 记忆 + 状态 + 决策"]
    Web --> Prompt
    Prompt --> ChatLLM["主聊天模型<br/>OpenAI 兼容"]
    ChatLLM --> Filter["输出过滤<br/>占位符 / sticker / life-update"]
    Filter --> API
    Filter --> Writeback["Live Memory 回写<br/>user_new / ai_generated"]
    Filter --> Schedule["可选定时任务提取<br/>schedule-hint"]
    Filter --> LifePatch["生活状态标记回写<br/>life-update"]
  end

  subgraph Ingestion["离线导入流水线"]
    message["导出的聊天记录 JSON"] --> Plugin["导入 plugin<br/>QQChatExporter / WeFlow"]
    Plugin --> Normalize["NormalizedMessage<br/>角色 / 类型 / 占位符"]
    Normalize --> Clean["清洗 / emoji 与表情占位 / @ 归一"]
    Clean --> Redact["PII 脱敏"]
    Redact --> Split["会话切分"]
    Split --> Chunk["三索引 chunk<br/>friend / window / response_pair"]
    Chunk --> Embed["Embedding 模型"]
    Chunk --> Label["可选打标签小模型"]
    Split --> PersonaBuild["persona / style / circadian 画像"]
  end

  subgraph Storage["本地持久化"]
    Lance[(LanceDB<br/>human_original / live / 关系记忆)]
    Persona["persona_card.md<br/>persona_style_profile.json"]
    Circadian["circadian_profile.json<br/>真实作息画像"]
    LifeFile["life_state.json<br/>当天生活时间线"]
    Assets["图片 / 表情包缓存<br/>stickers index + 文件"]
  end

  Embed --> Lance
  Label --> Lance
  PersonaBuild --> Persona
  PersonaBuild --> Circadian
  Retrieve --> Lance
  Relationship --> Lance
  Prompt --> Persona
  Life --> Circadian
  Life --> LifeFile
  LifePatch --> LifeFile
  Writeback --> Lance
  Schedule --> API
  API --> Assets
Loading
mindmap
  root((Afterglow))
    Afterglow 项目主体
      导入与清洗
      导入插件
      向量库(来源分层)
      混合检索
      Persona
      作息画像
      生活时间线
      关系记忆
      互动决策层
      联网能力
      表情包与图片缓存
      OpenAI 兼容 API
      诊断链路
    前端 测试调试
      聊天界面
      记忆溯源
      设置页
      诊断面板
    模型与服务
      主聊天模型
      Embedding 模型
      打标签小模型
      生活状态/网页意图小模型
      互动决策小模型(可选)
      Cross-encoder Reranker(可选)
      Tavily 或 SearXNG
Loading

关键设计

  • 三索引混合检索:response_pairs(用户输入→对方回复)+ 单条朋友发言 + 多轮窗口,RRF 融合。五路向量召回 + relationship_memory + life 在 Layer A 一次 asyncio.gather 并发完成,主链路只取最长那条耗时。
  • 可选 Cross-encoder 粗排:开启 CROSS_RERANK_ENABLED=true 后,RRF 召回的候选会先过一道专用 reranker 模型(如 Qwen3-Reranker-8B,二次元 API 中转站免费提供)按相关性精排,再注入主聊天 prompt——区分度比 LLM-as-reranker 更细,延迟仅 ~3s。
  • 可选 Query 改写:短句口语("在吗 / 想你了 / 好累")走 query rewrite 小模型展开为 1-3 个检索友好的变体,多变体命中按 (best_rank, distance) 合并,避免短 query 召回稀疏。
  • 可选自适应切分:导入期 CHUNKING_STRATEGY=adaptive 时按话题边界切聊天记录(启发式 / 小模型可选),比固定 12 条窗口更贴合自然话题;会话级并发可配,大库切分 30 秒内完成。
  • AI 回复连发分条:主聊天 LLM 输出含 \n\n 时前端拆成多条独立气泡(独立头像 / 时间戳 / 名字),后续段错峰 2-5s 显示,模拟真人 IM 连发节奏。后端协议 100% 标准 OpenAI 兼容,第三方客户端零适配。
  • 三层时间权重:近期消息略增(recency boost ±15% 封顶)+ 暖度词加权(warmth boost)+ live/history 信任分层。
  • 分层记忆防自污染:运行时把用户输入标记为 user_new,把 AI 分身回复标记为 ai_generated。两者都可用于连续性检索,但 ai_generated 低权重,且不会参与 persona / 风格蒸馏;真正用于模仿对方语气的长期证据只来自真人原始聊天(human_original)。
  • AI 回复长期累积可控:默认 AI_GENERATED_LONG_TERM_ENABLED=false,AI 回复只在同一会话内用于连续性;如果希望 AI 分身随着长期互动形成自己的变化轨迹,可以开启该项,让 ai_generated 跨会话参与低权重语义检索。
  • 持续生长记忆:每轮对话都异步回写 live_messages,向量库不再是一次性快照。
  • 本轮互动决策层:生成回复前先判断本轮该认真、安抚、撒娇、接梗、转移、沉默、发图还是表情;也会在用户烦躁、崩溃、失眠、关系压力等场景下禁止继续刺激用户。规则引擎兜底安全场景;如果开启 RESPONSE_POLICY_MODEL_ENABLED,会再叫一次小模型做有界微调(不能降级 risk、不能撤销规则给的安全/沉默/要图/要表情判断)。
  • 沉默与延迟响应:决策层判断本轮不应回复时直接短路,不调主模型,返回 finish_reason="silenced" + sentinel content + policy.should_reply=false;生活状态建议的拟人化延迟放在 policy.reply_delay_seconds,由客户端决定何时展示。
  • 真实时间 + 生活状态:每次模型调用都会收到当前时区下的真实时间;生活时间线由可配置小模型维护,回答"在干嘛/吃了吗/睡没睡"时优先使用当前状态。fail-open 兜底:LLM 调用失败也写入最小 fallback state,避免每轮死循环重试。
  • 可选联网:后端默认支持 Tavily,也可切到 SearXNG;在明确需要公开实时信息时把网页摘要注入 prompt,默认关闭。
  • 大库索引加速uv run python -m xuwen.ingestion.cli index 一键给 LanceDB 向量表建 IVF_PQ 索引,10 万行以上规模时检索耗时降一个数量级;cli optimize 定期合并增量入索引。
  • 零微调:完全靠 RAG + Prompt Engineering + Persona 卡片,不动模型权重。
  • 时光信笺 UI:米色信笺 + 黛蓝墨痕 + 思源宋体 + 暖光粒子 + 拟人化打字节奏 + 记忆溯源浮窗。

🚀 快速开始

0. 环境要求

工具 版本 用途 备注
Python ≥ 3.12 后端运行时 必需
Node.js ≥ 20 前端构建 仅用前端时需要;纯 API 用户可不装
uv latest Python 包管理 推荐
pnpm latest Node 包管理 仅前端
QQChatExporter V5 / WeFlow(微信,arkme-json)导出纯文本 JSON 真人聊天数据源 至少一份;plugin 会自动识别格式;WeFlow 是当前唯一可用的微信导入方案,但不再建议主动使用

1. 准备模型(API)

Afterglow 不内置模型,所有 LLM 调用都走你自己配置的 OpenAI 兼容服务。下表列出每个角色需要什么样的模型 + 推荐。

角色 必需? 作用 推荐 配置项
主聊天模型 ✅ 必需 生成最终回复,决定"像不像 TA" DeepSeek / Gemini OPENAI_BASE_URL OPENAI_API_KEY CHAT_MODEL
Embedding 模型 ✅ 必需 向量化历史聊天与检索 query Qwen3-Embedding-8B(阿里云 DashScope 免费额度,或合作伙伴二次元 API 中转站免费提供该模型) EMBEDDING_API_URL EMBEDDING_API_KEY EMBEDDING_MODEL EMBEDDING_DIM
打标小模型 🔧 可选 给历史 chunk 打 mood / topic / importance 软标签 智谱清言 glm-4-flash(免费) LABELING_ENABLED=true LABEL_API_* LABEL_MODEL
生活状态 / 网页意图小模型 🔧 可选 维护 AI 当前在做什么、判断要不要打开 URL 智谱清言 glm-4-flash(免费) LIFE_API_* LIFE_MODEL
互动决策小模型 🔧 可选 规则层之后再叫一次小模型微调本轮策略 智谱清言 glm-4-flash(免费) 或复用 LIFE_* RESPONSE_POLICY_MODEL_ENABLED=true RESPONSE_POLICY_*
Cross-encoder Reranker 🔧 可选 RRF 召回后按相关性精排,显著提升记忆贴合度 Qwen3-Reranker-8B(二次元 API 中转站 免费)/ bge-reranker-v2-m3 / DashScope gte-rerank CROSS_RERANK_ENABLED=true CROSS_RERANK_*
视觉模型 🔧 可选 主聊天模型不支持视觉时用 VLM 把图转文字 Qwen-VL / Gemini Vision VISION_API_* VISION_MODEL
联网检索 🔧 可选 用户明确要求"搜索 / 最新 / 天气"时调 Tavily(月度免费额度)/ 自建 SearXNG WEB_ACCESS_ENABLED=true WEB_SEARCH_*

💡 推荐组合(小模型几乎零成本)

  • 主聊天模型DeepSeekGemini——较高参数量才能撑得起"像 TA 说话"的细腻度
  • 所有小模型(打标 / 生活状态 / 网页意图 / 互动决策):智谱清言 glm-4-flash——免费且性能完全够辅助任务
  • EmbeddingQwen3-Embedding-8B(DashScope 免费额度 / 二次元 API 中转站 免费)
  • Cross-encoder Reranker(可选但强烈推荐):Qwen3-Reranker-8B(二次元 API 中转站 免费)—— 让"召回记忆是否真的相关"质量提一档

这套组合下你的主要花费只在主聊天模型上,其它链路几乎不消耗额度。

2. 启动后端

接下来所有命令都在 backend/ 目录下执行。

后端有两种配置方式,首次使用强烈推荐方式 A(配置向导)。

⚡ 方式 A:配置向导(推荐)

直接装依赖 + 跑后端,不用先准备 .env

cd backend
uv sync --extra dev
uv run uvicorn xuwen.chat_api.app:create_app --factory --reload

后端启动时会检测 .env 是否缺失或关键字段(SELF_UID / FRIEND_UID / OPENAI_API_KEY / EMBEDDING_API_KEY / XUWEN_API_KEY)未填,任一不齐就自动启用配置向导,控制台打印:

========================================
  检测到首次配置(缺少 SELF_UID、FRIEND_UID、OPENAI_API_KEY...)
  已自动启用配置 UI(仅本次会话)

  浏览器访问:http://127.0.0.1:8000/config/
  访问 token(generated):xxxxxxxxxxxxxx

  把这串 token 粘到向导第 1 步的输入框即可。
  配置完成并重启后,此提示将消失。
========================================

浏览器打开链接,粘 token,跟着 8 步走完:

  1. 身份信息 — 上传 QQ / 微信导出 JSON 自动识别 UID,不必手动找 u_xxx / wxid_xxx;同一个人跨平台的多 UID 支持一并归并
  2. 关系 — 朋友 / 恋人 / 亲人 / 同事 / 自定义
  3. 聊天 AI — DeepSeek / Gemini / 自定义中转站 / 本地 Ollama,选完填密钥当场测连通
  4. 向量服务 + 打标 — DashScope / SiliconFlow / 自定义;默认开启 GLM-4-Flash 打标(小白可关)
  5. 可选功能 — 生活时间线 / 视觉理解 / 联网搜索 / 互动决策(默认全关,按需开)
  6. 检索增强(可选) — Query 改写(短句口语场景)+ Cross-encoder Reranker(如 Qwen3-Reranker-8B)按相关性精排
  7. 导入聊天记录 — 选切分策略(固定窗口 / 自适应 adaptive,后者按话题边界切分),文件直传后端,进度+耗时+断点续传,自动生成 persona_card.md 和作息画像
  8. 访问密码 — 自动生成 XUWEN_API_KEY,写入 .env

向导走完会自动写 backend/.env(原文件会备份到 .env-backups/),不需要单独跑 analyze_persona.py — 持久化、画像、打标都集成进去了。

完成后 Ctrl+C 重启后端,向导自动关闭,进入正常聊天 API 模式。

想只改配置、不跑全套服务? 独立配置入口(端口 8765,启动 < 1 秒,不跑 LanceDB 等):

uv run python -m xuwen.web_ui

适合升级 / 临时改 key 等场景。

🔧 方式 B:手动配置 .env + CLI(适合自动化 / Docker 部署)

步骤 ①:安装依赖
cd backend
uv sync --extra dev
步骤 ②:配置 .env
cp .env.example .env

用编辑器打开 .env,按文件内注释填写:

  • 身份信息 —— SELF_NAME / SELF_UID / FRIEND_NAME / FRIEND_UID
    • QQ:SELF_UID / FRIEND_UID 填 QQChatExporter 导出 JSON 里的 selfUid / 对方 sender.uidu_xxx 形式)
    • 微信:填 WeFlow 导出 JSON 里 senders[]wxidwxid_xxx 形式);定位方法见 backend/README.md
    • 微信导入风险:WeFlow 是 Afterglow 目前支持的微信导入适配器,默认微信导入插件依赖它;但 WeFlow 不再开源,无法保证将来的安全性。当前不建议主动使用 WeFlow,除非你接受这是现阶段唯一可用方案。
    • FRIEND_* 永远是你想让 AI 模仿的那个人,不是你自己
    • 跨平台 / 多账号:同一个人有多个 UID(QQ + 微信 / 主号 + 小号),直接在 SELF_UID / FRIEND_UID用逗号分隔列上所有 UID,导入时会被视为同一身份。例:FRIEND_UID=u_qq_friend,wxid_friend_main,wxid_friend_alt
  • 主聊天模型 —— OPENAI_BASE_URL / OPENAI_API_KEY / CHAT_MODEL
  • Embedding 模型 —— EMBEDDING_API_URL / EMBEDDING_API_KEY / EMBEDDING_MODEL / EMBEDDING_DIM / EMBEDDING_BATCH_SIZE / EMBEDDING_MAX_CONCURRENCY / EMBEDDING_MAX_REQUESTS_PER_MINUTE
  • 本地访问密钥 —— XUWEN_API_KEY(长随机串;调用方在 Header 带 Authorization: Bearer <key>

⚠️ 关键提醒(容易踩的坑)

  • API_AUTH_REQUIRED=false 不会让 XUWEN_API_KEY 失效——只要 KEY 有值就强制校验。想完全开放,请同时把 KEY 留空。
  • 改了 .env 必须完全重启后端——uvicorn --reload 只监听 .py 文件变化,不会重新加载 .env
  • 客户端请求里的 model 字段是占位——实际使用的永远是 .env 配的 CHAT_MODEL;传 gpt-4.1gemini-flash 都会被忽略。
步骤 ③:导入历史聊天
# 单文件
uv run python -m xuwen.ingestion.cli import 路径/到/你的聊天记录.json

# 多文件(同一个朋友在 QQ + 微信都聊过、或者多个账号)
uv run python -m xuwen.ingestion.cli import qq_导出.json wechat_导出.json 小号_导出.json
  • CLI 自动按 JSON 顶层特征识别 QQ / WeFlow 格式,可任意混合
  • 跨平台 / 多账号场景:在 .envSELF_UID=u_qq,wxid_meFRIEND_UID=u_qq,wxid_friend逗号分隔)把全部 UID 列出来
  • 多文件按命令行顺序处理,共享 LanceDB 连接与 Embedding 客户端
  • 开启 LABELING_ENABLED=true 时会接着自动打标
  • 中断 / 限流失败不丢导入数据,之后可续跑:uv run python -m xuwen.ingestion.cli label

⚠️ 多文件场景的两个局限(重要)

  • 作息画像 (circadian_profile.json) 仅基于最后一个文件生成——把数据量最大或最具代表性的对话放在命令行最后一位,画像才能反映 TA 真实的作息分布。
  • 下一步 analyze_persona 当前也只接受单个 JSON——多文件场景下,建议挑那个最具代表性的(通常就是同一份"最后一位"文件)单独跑画像。LanceDB 向量库本身是合并的,检索能用上全部数据,但 persona 卡片不会跨文件合并。
步骤 ④:生成 persona 卡片 + 作息画像
uv run python scripts/analyze_persona.py 路径/到/你的聊天记录.json

🔍 必做这一步。 这一步生成四个文件到 PERSONA_DATA_DIR

  • persona_card.md — 长期语气画像(prompt 用)
  • persona_report.json — 完整统计报告
  • persona_style_profile.json — 按话题分桶的风格画像
  • circadian_profile.json — TA 的真实作息(识别夜猫子 / 跨时区 / 工作日 vs 周末)

注意:persona 是离线统计画像,只提供长期语气参考;当天在做什么由 life_state.json 和聊天时的小模型状态决定。

⚠️ 当前只接受单个 JSON 文件。 如果你在步骤 ③ 导入了多个文件,请挑数据量最大或最具代表性的那一份跑 persona——通常就是步骤 ③ 命令行里放在最后一位的那个文件(与 circadian 画像保持一致)。后续会支持多文件合并 persona,欢迎 PR。

步骤 ⑤:启动 chat API
uv run uvicorn xuwen.chat_api.app:create_app --factory --reload

→ 访问 http://127.0.0.1:8000

步骤 ⑥:健康检查
curl http://127.0.0.1:8000/healthz
curl -H "Authorization: Bearer <XUWEN_API_KEY>" http://127.0.0.1:8000/readyz

/healthz唯一不需要鉴权的端点,可用于容器存活探针。

3. 启动前端(可选)

cd frontend
pnpm install
pnpm dev

→ 打开 http://localhost:5173,按引导填写姓名/关系,就可以开始聊天了。

4. 不用前端?直接接入 OpenAI 兼容客户端

后端实现 OpenAI Chat CompletionsOpenAI Responses API 两套协议,可以接入任何 OpenAI 兼容客户端(Chatbox、Open WebUI、NextChat、ChatGPT Next Web 等)。

curl -X POST http://127.0.0.1:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <XUWEN_API_KEY>" \
  -d '{
    "messages": [{"role": "user", "content": "在吗"}],
    "conversation_id": "my-conv-1"
  }'

💡 关于 stream 字段

Afterglow 默认关闭真流式RESPONSE_STREAMING_ENABLED=false),因为真人发消息从来不是逐字蹦出来的。客户端传 stream=true 时仍按 OpenAI SSE 协议返回,但一次性发完整消息——OpenAI 兼容性 100% 保留,用户视觉上看到的是"一整条消息突然出现"。


🐳 用 Docker 部署(可选,替代源码方式)

如果你不想装 Python / uv / 一堆依赖,直接用 Docker 镜像一行起服。和源码部署共享 .env.data/,任意时候切换互不影响。

角色一:最终用户(不需要克隆仓库)

mkdir -p ~/afterglow && cd ~/afterglow

# 拿一个 compose 文件即可,约 50 行
curl -O https://raw.githubusercontent.com/kldhsh123/Afterglow/main/docker/compose.standalone.yaml
mv compose.standalone.yaml compose.yaml

docker compose pull          # 从 GHCR 拉公开镜像(amd64 / arm64 都有)
docker compose up -d

容器首次启动会自动检测到挂载目录是空的,进入配置向导首次模式。看一次性 setup token:

docker compose logs backend | grep -iE "token|/config/"

浏览器打开 http://localhost:8000/config/,把 token 粘进向导第 1 步,7 步走完即生效。向导写入的 .env 直接落到 ~/afterglow/.env,备份在 .env-backups/,重启即用。

完成后目录结构:

~/afterglow/
├── compose.yaml
├── .env                     ← 向导生成,含完整注释
├── .env.example             ← 从镜像拷出的模板
├── .env-backups/            ← 历次配置变更
└── .data/
    ├── lancedb/             ← 向量库
    ├── persona/             ← 人格卡片
    ├── stickers/            ← 表情包
    ├── images/              ← 图片缓存
    └── uploads/             ← 配置向导上传暂存

角色二:开发者(仓库内)

git clone https://github.com/kldhsh123/Afterglow.git
cd Afterglow
docker compose build         # 第一次构建,3-8 分钟
docker compose up -d

默认挂载仓库内 ./backend/,与源码部署共享 .env.data/。 想把数据放仓库外:

cp .env.docker.example .env.docker
# 编辑 AFTERGLOW_DATA_DIR=/var/lib/afterglow
docker compose --env-file .env.docker up -d

日常运维命令

# 看日志
docker compose logs -f backend

# 改了 .env 必须重启(pydantic-settings 不监听文件变化)
docker compose restart backend

# 拉新版镜像 + 滚动更新
docker compose pull && docker compose up -d

# 导入聊天记录(把待导入 JSON 放到挂载目录下任何子路径)
docker compose run --rm backend \
  python -m xuwen.ingestion.cli import .imports/qq.json

# 大库建索引
docker compose run --rm backend python -m xuwen.ingestion.cli index

# 临时停 / 完全停(数据保留)
docker compose stop
docker compose down

# 进容器排错
docker compose exec backend bash

前端怎么办

镜像里只有后端。前端继续按 #3pnpm dev 或者自己用 Nginx 托管 frontend/dist。后端容器对外暴露 8000,前端发到这个端口即可。

与源码部署互操作

完全可以并存,只要别同时跑(端口冲突):

# 今天用源码模式
cd backend && uv run uvicorn xuwen.chat_api.app:create_app --factory --reload

# 关掉后明天换 Docker
docker compose up -d

两边读写同一份 backend/.envbackend/.data/无需任何迁移

几个 Docker 模式特有的注意点

  • 首次冷启动会自动注入 CONFIG_UI_LOCALHOST_ONLY=false 到挂载目录的 .env,因为容器外浏览器访问 /config/ 时请求源 IP 是 docker 网桥,会被 localhost-only 拒。配置完成后想重新收紧的话手动改回 true 即可。
  • 配置向导写入是 atomic rename,直接落到宿主机 .env——不是写在容器层,重启不丢。
  • 容器 entrypoint 会动态对齐 uid/gid 到挂载目录所有者,避免 WSL drvfs / Linux 原生 / NFS 等跨 uid 场景下向导写文件 EPERM 报错。
  • .env 后必须 docker compose restart,与源码模式一致——pydantic-settings 只在启动时加载。

故障排查

现象 大概率原因 处理
向导 PUT /config/values 500 + EPERM 挂载目录 uid 与容器内不一致,且 entrypoint 没对齐成功 看启动日志有没有 [entrypoint] 调整 afterglow 用户匹配;没有就贴日志反馈 issue
LanceDB deprecation warning 刷屏 上游 lance crate 的弃用提示,不影响功能 .envRUST_LOG=lance=error 屏蔽

🎨 自定义人格模板

5 个内置预设:xuwen(默认)/ friend / lover / family / colleague,在 .env 设:

PERSONA_TEMPLATE=lover

完全自定义:

PROMPT_TEMPLATE_DIR=/path/to/your/templates
PERSONA_TEMPLATE=my_template
# 会去 /path/to/your/templates/my_template.md.j2 读

模板可用变量:friend_name / self_name / relationship_description / persona_card / retrieved_friend_examples / retrieved_dialogue_windows / recent_conversation / current_user_message / today / current_date / current_time / current_datetime / current_weekday / timezone。其中 retrieved_friend_examples 会优先包含 response_pairs 样例。persona_card 只应作为长期语气参考,不要当作当天事实来源。


📁 项目结构

Afterglow/
├── backend/                 # Python 后端(FastAPI + LanceDB + RAG)
│   ├── xuwen/
│   │   ├── core/            # 领域模型、错误类型、时间工具
│   │   ├── ingestion/       # 解析、清洗、PII 脱敏、切分、chunking、向量化
│   │   ├── memory/          # LanceDB schema、CRUD、检索融合、回写队列、记忆来源策略
│   │   ├── persona/         # 离线人格画像、prompt 模板(Jinja2)
│   │   ├── companion/       # 生活时间线、关系记忆、本轮互动决策层
│   │   ├── chat_api/        # FastAPI 服务(OpenAI 兼容)
│   │   └── web_ui/          # 配置向导(首次模式自动启用)+ 静态资源
│   ├── web_ui_src/          # 配置向导前端源码(Vue + Vite,构建到 xuwen/web_ui/static/)
│   ├── scripts/             # 离线脚本(导入、画像、检索评估)
│   ├── pyproject.toml
│   └── README.md            # 后端详细文档
│
├── frontend/                # Vue 3 + Vite 前端(时光信笺)
│   ├── src/
│   │   ├── api/             # SSE / fetch 封装
│   │   ├── components/      # chat / memory / common / layout / onboarding
│   │   ├── composables/     # useTypewriter / useAutoScroll / markdown
│   │   ├── stores/          # Pinia: settings / chat / memory
│   │   └── views/           # HomeView / SettingsView
│   ├── tailwind.config.js
│   ├── package.json
│   └── README.md            # 测试/调试前端说明
│
└── 开发缓存/                 # 你的 QQ 导出 JSON(.gitignore)

❓ FAQ

Q:什么是"配置向导"?必须用吗? A:可选但推荐首次用户用。后端启动时若发现关键字段(SELF_UID / OPENAI_API_KEY / EMBEDDING_API_KEY / XUWEN_API_KEY)任一未填,会自动启用 /config/ 网页向导,控制台打印一次性 token;浏览器跟着 7 步走完即可生成完整 .env、导入聊天记录、生成 persona 卡片。已经配过的人启动后不会触发向导。完全不想用 UI 的可以照「方式 B」手动改 .env

Q:必须用阿里云 Qwen3-Embedding 吗? A:不必,任何 OpenAI 兼容的 /embeddings 接口都可以。改 EMBEDDING_API_URL / EMBEDDING_MODEL / EMBEDDING_DIM 即可。本地 ollama 也支持。

Q:必须用 OpenAI 吗? A:不必,任何 OpenAI 兼容的 /chat/completions 接口都可以(DeepSeek、Moonshot、Qwen、ollama 等)。改 OPENAI_BASE_URL 即可。

Q:能不能不脱敏 PII? A:可以。.envENABLE_PII_REDACTION=false,或通过 PII_RULES_PATH 加载自定义规则。

Q:QQ 号 / URL 为什么不脱敏? A:QQ 号在导出文件里到处都是(uid 关联需要);URL 是对话语境的一部分(朋友分享 B 站视频是有意义的)。脱敏列表只覆盖一旦泄漏就造成实质损失的"硬隐私"。

Q:能否导入微信 / Telegram / Discord 数据? A:已内置两个导入 plugin —— QQChatExporter V5(QQ)和 WeFlow arkme-json(微信)。WeFlow 是 Afterglow 所支持的微信导入适配器,Afterglow 的默认微信导入插件依赖此项目;但我注意到 WeFlow 不再开源,所以我无法保证 WeFlow 将来的安全性。在不久的将来我需要 WeFlow 的替代方案来确保用户隐私安全;在此期间,我不会建议使用 WeFlow,但这是目前唯一可用的方案。CLI 会按 JSON 顶层特征自动识别,无需额外参数。导出时记得只勾选纯文本,不要带图片/语音/文件等附件。其它平台目前没有内置 plugin,但写一个新 plugin 输出 NormalizedMessage 即可,下游流水线无需改动,欢迎 PR。

Q:会不会越聊越不像? A:每轮对话都会异步回写到 live_messagestrust_level=0.35,权重远低于历史 1.0)。前端可在设置页"暂停回写"避免污染。

Q:怎么删除某条记忆? A:调 DELETE /memory/friend_messages/{chunk_id}DELETE /memory/response_pairs/{pair_id}(软删除)。

Q:能本地完全离线吗? A:可以。LLM 用 ollama / vLLM;embedding 用 nomic-embed-text / bge 等本地模型。


🛠️ 开发

# 后端
cd backend

# 前端
cd frontend
pnpm dev                        # 开发服务器
pnpm build                      # 类型检查 + 生产构建

更多文档:


📜 License

AGPL-3.0-or-later


Star History Chart

如果 Afterglow 帮你留住了一些温度,欢迎点一颗 ⭐。

About

使用 QQ/微信 聊天记录结合向量数据库让AI更好的扮演对方的角色,在不微调模型的情况下可以达到可观的效果。把曾经的美好,续成往后的陪伴。 --续温

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

Packages

 
 
 

Contributors