No description
  • Python 75.1%
  • Go 19.4%
  • HTML 3.8%
  • Dockerfile 0.8%
  • PowerShell 0.5%
  • Other 0.4%
Find a file
VesperGlow 4239abb2b5
Some checks failed
Build, test and publish / Test Python and Go (push) Successful in 43s
Build, test and publish / Build and publish app (push) Failing after 6s
Build, test and publish / Build and publish qqbot (push) Failing after 5s
ci: publish images with Forgejo credentials
2026-06-29 20:12:14 +08:00
.github/workflows ci: publish images with Forgejo credentials 2026-06-29 20:12:14 +08:00
app Make short-term history ordering deterministic 2026-06-29 17:31:05 +08:00
deploy/quadlet Move persona to app-level PERSONA_PROMPT (single source) 2026-06-29 17:22:28 +08:00
qqbot Move persona to app-level PERSONA_PROMPT (single source) 2026-06-29 17:22:28 +08:00
scripts Build containerized QQ graph memory agent 2026-06-29 00:05:37 +08:00
tests Build containerized QQ graph memory agent 2026-06-29 00:05:37 +08:00
.dockerignore Build containerized QQ graph memory agent 2026-06-29 00:05:37 +08:00
.env.example Move persona to app-level PERSONA_PROMPT (single source) 2026-06-29 17:22:28 +08:00
.gitignore Add global persona + MCP tools, slim to private-chat only 2026-06-29 15:36:08 +08:00
compose.gpu.yaml Build containerized QQ graph memory agent 2026-06-29 00:05:37 +08:00
compose.yaml Use hosted Firecrawl MCP URL and ${ENV} key expansion 2026-06-29 16:09:51 +08:00
README.md Move persona to app-level PERSONA_PROMPT (single source) 2026-06-29 17:22:28 +08:00

Qwen + Neo4j 长期记忆助手

Build and test

这是一个可直接容器化部署的个人情感陪伴助手:本地 Qwen3-Embedding-0.6B 负责向量化Neo4j 同时保存向量、记忆节点和图谱关系;便宜模型筛选长期记忆、抽取情绪,主模型负责对话与工具调用;腾讯官方 BotGo 桥接容器负责与 QQ 私聊C2C通信。

flowchart LR
    U[用户 / API] --> A[Memory Agent]
    A -->|当前消息| E[本地 Qwen3 Embedding]
    E -->|1024 维向量| N[(Neo4j)]
    A -->|筛选记忆| S[便宜模型]
    A -->|对话 + 工具| L[主模型]
    S -->|结构化记忆 + 情绪| A
    L -->|记忆工具 + MCP 外部工具| A
    M["远程 MCP: Tavily/Firecrawl"] <-->|联网搜索/抓取| L
    N -->|时序加权检索 + 图谱| A
    Q[QQ 用户] <-->|WebSocket / HTTPS Webhook| B[BotGo 桥接]
    B -->|内部 /v1/chat| A
    B -->|被动回复| Q

功能总览

  • 双模型记忆:便宜模型自动筛选值得长期记住的内容,主模型对话并按需调用记忆工具。
  • 图谱 + 向量记忆Neo4j 同时存 1024 维向量与图谱关系,所有记忆按 user_id 隔离。
  • 时序加权检索Cypher 25 SEARCH 子句向量召回,叠加新近度、重要性、访问强化排序(见数据结构)。
  • 记忆演变SUPERSEDES:用户情况变化时新记忆取代旧记忆并保留可回溯的时间线。
  • 记忆主体subject:区分“关于用户”与“助手自己的承诺 / 人设”,检索时分组呈现、互不混淆。
  • 情绪时间线:从对话抽取情绪成链(HAS_MOOD/NEXT_MOOD),让助手感知跨会话情绪趋势。
  • 分层提示词人设层app 级 PERSONA_PROMPT 可整体替换口吻,对所有入口生效)与系统指令层(输出格式如禁用 Markdown、记忆/工具、安全)分离,系统指令始终生效、优先于人设。
  • MCP 工具:通过 MCP_SERVERS_JSON 接入 Tavily 联网搜索、Firecrawl 网页抓取等远程 MCP 服务器(见 MCP 工具)。
  • 纯私聊定位:个人情感陪伴,只处理 QQ 私聊C2C不支持群聊与频道。
  • 零本地依赖部署:宿主机仅需 Docker镜像由 GitHub Actions 编译并发布到 GHCR。

最快启动

宿主机只需要 Docker不需要 Python、Java、Neo4j 或模型运行环境。

  1. 安装 Docker EngineLinux VPS或 Docker DesktopWindows/macOS并确认 docker compose version 能运行。

  2. 进入本目录,复制配置:

    cp .env.example .env
    
  3. 编辑 .env,至少填写:

    AI_BASE_URL=https://你的供应商地址/v1
    AI_API_KEY=你的key
    MEMORY_MODEL=便宜模型名
    CHAT_MODEL=支持工具调用的主模型名
    APP_API_KEY=一段长随机字符串
    NEO4J_PASSWORD=另一段长随机字符串
    QQ_APP_ID=QQ开放平台的AppID
    QQ_APP_SECRET=QQ开放平台的AppSecret
    
  4. CPU 启动:

    docker compose up -d --build
    

    NVIDIA GPU已装驱动和 NVIDIA Container Toolkit

    docker compose -f compose.yaml -f compose.gpu.yaml up -d --build
    
  5. 查看首次下载与启动进度:

    docker compose logs -f embedding app
    

模型首次启动会下载约 GB 级文件。完成后访问 http://127.0.0.1:8000 使用简易聊天页API 文档在 http://127.0.0.1:8000/docsNeo4j Browser 在 http://127.0.0.1:7474

VPS 默认仅监听 127.0.0.1,建议用 SSH 隧道或反向代理加 HTTPS不要直接把 Neo4j 端口暴露到公网。确需对外提供应用 API 时,把 APP_BIND_IP 改为 0.0.0.0

关键环境变量

变量 默认值 用途
AI_BASE_URL OpenAI-compatible API 根地址,代码会拼接 /chat/completions
AI_API_KEY AI 提供商密钥
MEMORY_MODEL 便宜的记忆筛选模型
CHAT_MODEL 对话和工具调用模型
PERSONA_PROMPT 全局人设app 级),只写性格/口吻,对 QQ/网页/API 全部生效;留空用内置默认。输出格式、记忆工具与安全属独立的系统指令层,始终生效
EMBEDDING_BASE_URL http://embedding:80 Embedding 接口地址
EMBEDDING_API_STYLE tei teiopenai
EMBEDDING_MODEL Qwen/Qwen3-Embedding-0.6B 本地或远程模型名
EMBEDDING_DIMENSIONS 1024 Neo4j 向量索引维度Qwen 支持 321024
EMBEDDING_CONTEXT_SIZE 32768 Embedding 最大批 token 数;小内存 VPS 建议 4096/8192
MEMORY_RECENCY_WEIGHT 0.15 时序加权检索新近度加成权重0=关闭,纯相似度排序)
MEMORY_IMPORTANCE_WEIGHT 0.10 时序加权检索:重要性加成权重
MEMORY_RECENCY_HALFLIFE_DAYS 30 新近度衰减半衰期(天);越小越偏向近期记忆
NEO4J_URI bolt://neo4j:7687 Neo4j Bolt 地址
APP_API_KEY 此服务自己的 Bearer Token公网部署必须配置
QQ_APP_ID QQ 开放平台机器人 AppID
QQ_APP_SECRET QQ 机器人 AppSecret用于 Access Token 和 Webhook 验签
QQ_EVENT_MODE webhook QQ 事件接入模式:websocketwebhook
QQ_WEBHOOK_PATH /qqbot Webhook 模式下的 QQ 平台回调路径
MCP_SERVERS_JSON [] 远程 MCP 工具服务器列表详见下方「MCP 工具」

机器人定位为个人情感陪伴,仅处理 QQ 私聊C2C不支持群聊与频道。

维度一旦建好索引便不能原地修改。要改变 EMBEDDING_DIMENSIONS,需要删除旧向量索引并重新生成全部记忆向量;全新测试环境也可以用 docker compose down -v 清空数据后重建(这会永久删除全部 Neo4j 数据和模型缓存)。

对话 API

curl http://127.0.0.1:8000/v1/chat \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer 你的APP_API_KEY' \
  -d '{
    "user_id": "sorak",
    "message": "请记住,我偏好简洁的中文回答。"
  }'

响应会包含:

  • message:主模型回答;
  • retrieved_memories:本轮向量检索命中的记忆;
  • saved_memories:便宜模型本轮自动筛选并保存的记忆;
  • tool_events:主模型调用过的记忆工具;
  • conversation_id:后续请求带回即可保留短期对话历史。

主要接口:

  • POST /v1/chat:对话;
  • POST /v1/memories:手工写入记忆;
  • GET /v1/memories/search:语义搜索;
  • GET /v1/memories/recent:最近记忆;
  • DELETE /v1/memories/{id}:软删除/遗忘;
  • GET /v1/memories/{id}/history:沿 SUPERSEDES 链回溯一条记忆的演变时间线;
  • POST /v1/memories/link:建立记忆关系;
  • GET /v1/mood/{user_id}:情绪时间线与近期趋势聚合;
  • GET /v1/graph/{user_id}:导出小型图谱快照;
  • GET /health:检查三项依赖。

MCP 工具(联网搜索 / 网页抓取)

主模型除了内置的记忆工具,还可以调用远程 MCP 服务器提供的工具。通过 MCP_SERVERS_JSONJSON 数组)配置,每项字段:

  • name(必填):服务器标识,工具会以 mcp__<name>__<tool> 暴露给模型;
  • url必填MCP 服务器地址,可用 ${NAME} 引用环境变量(便于只在 env 填 key
  • transportstreamable_http(默认)或 sse
  • headers:可选请求头对象,同样支持 ${NAME}
  • tools / exclude:工具名白名单 / 黑名单,按需挑选以节省 token
  • enabled:设为 false 可临时停用某项。

Tavily 与 Firecrawl 均提供托管的 streamable-http 端点,把 API key 单独放进环境变量、URL 里用 ${...} 引用即可。下面这组只注册 Tavily 联网搜索与 Firecrawl 网页抓取,避免功能重叠浪费 token

TAVILY_KEY=tvly-你的KEY
FIRECRAWL_KEY=fc-你的完整APIKEY
MCP_SERVERS_JSON=[{"name":"tavily","url":"https://mcp.tavily.com/mcp/?tavilyApiKey=${TAVILY_KEY}","tools":["tavily_search"]},{"name":"firecrawl","url":"https://mcp.firecrawl.dev/${FIRECRAWL_KEY}/v2/mcp","tools":["firecrawl_scrape"]}]

CHAT_MODEL 需支持 OpenAI tool callingGET /healthmcp_tools 字段会显示已注册的 MCP 工具数量。

数据结构

  • (User)-[:HAS_CONVERSATION]->(Conversation)-[:HAS_MESSAGE]->(Message) 保存短期对话历史;
  • (User)-[:HAS_MEMORY]->(Memory) 保存长期记忆和向量;
  • (Memory)-[:MENTIONS]->(Entity) 形成实体图谱;
  • (Memory)-[:RELATED_TO {kind}]->(Memory) 保存主模型建立的记忆关系;
  • (Memory)-[:SUPERSEDES {at}]->(Memory) 记录记忆演变:用户情况变化时新记忆取代旧记忆,旧的软停用但保留在图里,可经 /history 回溯时间线;
  • (User)-[:HAS_MOOD]->(Mood)-[:NEXT_MOOD]->(Mood) 情绪时间线从每条消息抽取的情绪label/valence/note按时间成链。

情绪识别折叠进"记忆筛选"那一次廉价模型调用里(不额外耗 token仅在消息明确流露情绪时记录。每轮对话前会把近期情绪趋势压成一行注入上下文让助手自然体察用户状态。由 MOOD_TRACKING_ENABLED 开关、MOOD_TREND_DAYS 控制回看窗口。

所有记忆操作都按 user_id 隔离。遗忘采用软删除,节点仍可审计但不会再被检索。

每条记忆带 subject 区分主体:user(关于用户的事实/偏好,自动筛选只产出这类)与 assistant(助手自己对用户的承诺、约定或人设设定)。检索时按主体分组呈现给模型、互不混淆;写入会按主体隔离去重;旧数据无该字段时默认视为 user

检索使用 Cypher 25 的 SEARCH 子句做向量召回(取代已弃用的 db.index.vector.queryNodes,需 Neo4j 2026.02+),再在图内叠加时序加权:综合相似度、新近度(以 last_seen_at 为锚做半衰期衰减,被反复提及的记忆更"新鲜")、重要性与访问次数排序。权重见上方 MEMORY_*_WEIGHT,全设 0 即退回纯相似度。返回给上层的 score 仍是原始余弦相似度。

资源建议

  • CPU 部署:建议至少 4 核、8 GB RAM内存较小时先把 EMBEDDING_CONTEXT_SIZE=4096
  • GPU 部署:需要支持的 NVIDIA GPU、正确驱动及 Container Toolkit。
  • 磁盘:建议预留至少 810 GB 给镜像、模型缓存和数据库。

备份与更新

Neo4j 数据和模型分别保存在 Docker volume neo4j_datamodel_cache。不要把 docker compose down -v 当成普通停止命令;日常停止使用:

docker compose stop

查看错误:

docker compose ps
docker compose logs --tail=200 app embedding neo4j

如果主模型供应商不接受 tools 参数,服务会保留自动向量检索并降级为普通对话,同时在 warnings 中说明;要完整使用“记住/遗忘/关联”工具,应选择支持 OpenAI tool calling 格式的模型。

接入 QQ 机器人

本项目使用腾讯官方 tencent-connect/botgo v0.2.1,自动用 AppID + AppSecret 获取并刷新 Access Token并可通过 QQ_EVENT_MODE 在 WebSocket 与 HTTPS Webhook 之间切换。

  1. QQ 开放平台 创建机器人,把 AppIDAppSecret 写入 .env

  2. 使用 WebSocket 时设置以下变量。它由容器主动连接 QQ不需要公网域名或反向代理

    QQ_EVENT_MODE=websocket
    
  3. 使用 Webhook 时设置 QQ_EVENT_MODE=webhook,并给 qqbot:9000 配置公网 HTTPS 反向代理。默认宿主机只监听 127.0.0.1:9000,例如 Nginx

    location /qqbot {
        proxy_pass http://127.0.0.1:9000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }
    
  4. Webhook 模式下,在 QQ 开放平台把回调地址配置为 https://你的域名/qqbot。平台会发起签名校验BotGo 会自动完成响应。

  5. 本项目仅订阅私聊事件 C2C_MESSAGE_CREATE(个人情感陪伴定位,不处理群聊与频道)。

  6. 把 VPS 的固定公网出口 IP 加入机器人 IP 白名单;机器人上线前,在开放平台配置沙箱成员。

  7. 检查桥接状态和日志:

    curl http://127.0.0.1:9000/healthz
    docker compose logs -f qqbot
    

Webhook 收到事件后会立即确认,再异步调用 AI避免慢模型触发平台重试WebSocket 会自动维护会话、心跳和重连。两种模式共用同一套消息处理逻辑:按 msg_id 去重、按用户会话串行处理,并用 msg_seq 对长回复分片。QQ 的 OpenID 只以稳定哈希形式写入 Neo4j不直接保存原始 OpenID。

目前 QQ 附件只会得到“暂不支持”的文字提示;文本消息、记忆检索、自动记忆和主模型工具调用均完整接通。

GHCR 镜像

main 分支通过测试后GitHub Actions 会发布两个 linux/amd64 镜像:

ghcr.io/vesperglow/qq-agent-app:latest
ghcr.io/vesperglow/qq-agent-qqbot:latest

每次发布也会生成 sha-<完整提交号> 标签,生产环境可以锁定该标签,避免 latest 变化。

在 VPS 上使用预构建镜像:

cp .env.example .env
# 编辑 .env 后:
docker compose pull
docker compose up -d --no-build

GHCR 首次发布的个人包通常是私有的。私有状态下,先创建带 read:packages 权限的 GitHub PAT然后登录

echo "$GHCR_TOKEN" | docker login ghcr.io -u VesperGlow --password-stdin

如需免登录拉取,请分别进入 app 包设置qqbot 包设置,将可见性改为 Public

本地开发仍可使用 docker compose up -d --buildCompose 会按 APP_IMAGEQQBOT_IMAGE 给本地构建结果打标签。