前两篇分别记录了项目搭建和 PySide6 聊天窗口的实现。这一篇不继续单独展开某一个模块,而是做一次阶段性整理:把目前代码里已经实现的功能完整梳理一遍。

AICompanion 现在还不是最终形态,还没有 TTS,也还没有 Live2D。但它已经不只是一个空窗口了。目前这个版本已经把桌面聊天、模型接口、本地存储、长期记忆、情绪状态和主动消息的基础流程都接了起来。

1. 当前项目状态

当前项目路径是:

D:\Study\AICompanion

项目采用 src layout,核心代码放在:

src/aicompanion/

目前主要模块如下:

src/aicompanion/
  app.py                  # 应用装配入口
  config.py               # 配置读取
  controllers/
    chat_controller.py     # 聊天流程控制
  domain/
    character.py           # 角色设定
    emotion.py             # 情绪状态
  llm/
    base.py                # LLM 统一接口
    mock_client.py         # 本地模拟模型
    openai_compatible_client.py
    factory.py             # 根据配置创建模型客户端
  services/
    memory_service.py      # 长期记忆服务
    proactive_service.py   # 主动消息定时服务
  storage/
    schema.py              # SQLite 表结构
    database.py            # SQLite 数据访问层
  ui/
    main_window.py         # PySide6 主窗口
    chat_worker.py         # 后台线程工作对象

这套结构的目标是:UI 只管界面,Controller 负责流程,Service 负责领域功能,Storage 负责数据库,LLM 层负责模型调用。

2. 应用启动流程

程序入口依然是根目录下的:

main.py

它只做一件事:调用 aicompanion.app.main()

真正的应用装配在:

src/aicompanion/app.py

启动时会完成这些步骤:

  1. 读取 .env 和系统环境变量。
  2. 创建 QApplication
  3. 初始化 SQLite 数据库。
  4. 创建 MemoryService
  5. 根据配置创建 LLM 客户端。
  6. 创建 ChatController
  7. 创建 MainWindow
  8. 启动主动消息定时服务。
  9. 显示窗口。

对应代码逻辑大致是:

settings = load_settings()
app = QApplication(sys.argv)

database = Database(settings.database_path)
database.initialize()

memory_service = MemoryService(database)
llm_client = create_llm_client(settings)
controller = ChatController(database, memory_service, llm_client)

window = MainWindow(controller=controller)

proactive_service = ProactiveMessageService(controller=controller, parent=window)
proactive_service.message_ready.connect(window.append_assistant_message)
proactive_service.start()

window.show()

这里的 app.py 不写具体业务逻辑,只负责把各个模块接起来。这样后续如果要替换模型、换数据库实现、加新服务,都不会影响启动入口太多。

3. 配置系统

项目使用 python-dotenv 读取 .env

配置集中在:

src/aicompanion/config.py

目前支持的配置项包括:

APP_NAME
DATABASE_PATH
LLM_PROVIDER
LLM_BASE_URL
LLM_API_KEY
LLM_MODEL
LLM_TEMPERATURE
LLM_TIMEOUT_SECONDS

对应的默认开发配置是:

APP_NAME=AICompanion
DATABASE_PATH=data/aicompanion.sqlite3
LLM_PROVIDER=mock
LLM_BASE_URL=https://api.openai.com/v1
LLM_API_KEY=
LLM_MODEL=gpt-4o-mini
LLM_TEMPERATURE=0.8
LLM_TIMEOUT_SECONDS=60

这里比较重要的是 LLM_PROVIDER

当前支持:

  • mock:本地模拟回复,不需要 API key。
  • openai_compatible:调用 OpenAI-compatible Chat Completions 接口。
  • zhipu_compatible:目前也走兼容接口。

默认使用 mock,所以即使没有 API Key,也可以先开发窗口、数据库和消息流程。

4. PySide6 聊天窗口

当前桌面界面由 MainWindow 实现:

src/aicompanion/ui/main_window.py

窗口已经实现了这些基础功能:

  • 显示标题 AICompanion
  • 显示聊天记录列表。
  • 提供输入框。
  • 提供发送按钮。
  • 支持回车发送。
  • 用户消息靠右显示。
  • AI 消息靠左显示。
  • 发送中禁用输入框和按钮。
  • 回复结束后恢复输入。
  • 启动时加载最近聊天记录。

目前聊天列表使用 QListWidget,每一条消息是一个 QListWidgetItem

item = QListWidgetItem(f"{sender}: {content}")
item.setTextAlignment(
    Qt.AlignmentFlag.AlignRight if align_right else Qt.AlignmentFlag.AlignLeft
)
self.chat_list.addItem(item)

这个版本的 UI 还比较朴素,但已经能支撑完整聊天流程。后面可以继续升级为气泡样式、头像、角色立绘区域,最终再接 Live2D。

5. 后台线程与流式回复

聊天程序不能在主线程里直接请求模型。

如果模型 API 比较慢,而请求又发生在 PySide6 主线程中,窗口就会卡住。所以当前项目加入了:

src/aicompanion/ui/chat_worker.py

ChatWorker 运行在 QThread 中,负责调用:

self.controller.stream_user_message(self.user_message)

它通过 Signal 把结果发回 UI:

token_received = Signal(str)
finished = Signal()
failed = Signal(str)

主窗口收到 token 后,会逐段更新当前 AI 消息:

self.current_assistant_text += token
self.current_assistant_item.setText(
    f"Emilia: {self.current_assistant_text}"
)

这就形成了类似“AI 正在打字”的流式显示效果。

即使当前使用的是 mock 模型,也会把回复拆成小片段返回,用来模拟真实 API 的流式输出。

6. 聊天流程控制器

聊天流程集中在:

src/aicompanion/controllers/chat_controller.py

这是目前项目中比较核心的模块。

用户发送一条消息后,完整流程大致如下:

MainWindow
  -> ChatWorker(QThread)
  -> ChatController.stream_user_message()
  -> 保存 user 消息到 SQLite
  -> 更新情绪状态
  -> 尝试提取长期记忆
  -> 读取最近聊天记录
  -> 构建 system prompt
  -> 调用 LLMClient.generate_reply_stream()
  -> UI 逐段显示 token
  -> 保存完整 assistant 回复到 SQLite

也就是说,Controller 不只是转发消息,它还负责把记忆、情绪、历史记录和大模型调用串成一个完整流程。

当前 Controller 里保留了两个发送接口:

  • send_user_message():同步生成完整回复。
  • stream_user_message():流式生成回复,桌面 UI 默认使用这个。

同步接口可以留给后续测试或命令行调试使用。

7. LLM 抽象与本地 mock

大模型接口定义在:

src/aicompanion/llm/base.py

其中定义了两个核心概念:

@dataclass(frozen=True)
class ChatTurn:
    role: str
    content: str

以及统一的模型客户端接口:

class LLMClient(Protocol):
    def generate_reply(self, messages, system_prompt) -> str:
        ...

    def generate_reply_stream(self, messages, system_prompt):
        ...

这样 UI 和 Controller 不需要关心具体模型供应商。

当前本地 mock 客户端在:

src/aicompanion/llm/mock_client.py

它会读取最近一条用户消息,然后返回模拟回复:

我听到啦。你刚才说:...

现在我还是本地模拟回复,下一步我们会把这里替换成真正的大模型 API。

mock 客户端的价值很大:

  • 不需要 API Key。
  • 不需要网络。
  • 调试 UI 更方便。
  • 可以验证数据库保存流程。
  • 可以模拟流式输出。

这让项目在早期不依赖真实模型服务,也能继续开发。

8. OpenAI-compatible API 客户端

真实模型调用代码在:

src/aicompanion/llm/openai_compatible_client.py

它调用的是常见的 Chat Completions 兼容接口:

POST {LLM_BASE_URL}/chat/completions

请求体中会包含:

  • model
  • messages
  • temperature
  • stream

消息列表会先放入一条系统提示词:

api_messages = [{"role": "system", "content": system_prompt}]

再追加最近的用户和 assistant 历史消息。

当前客户端支持两种模式:

  • 非流式:generate_reply()
  • 流式:generate_reply_stream()

流式响应按 SSE 格式解析,遇到:

data: [DONE]

就结束输出。

项目这里暂时没有引入额外 SDK,而是使用 Python 标准库 urllib。这样依赖更少,也更容易看清楚一次模型请求到底发出了什么。

9. SQLite 本地存储

数据库表结构定义在:

src/aicompanion/storage/schema.py

当前已经有四张表:

chat_messages
memories
reminders
emotion_state

其中:

表名 作用
chat_messages 保存用户和 AI 的聊天记录
memories 保存长期记忆
reminders 保存任务提醒,当前先预留表结构
emotion_state 保存角色情绪状态

数据库访问集中在:

src/aicompanion/storage/database.py

目前已经实现:

  • 初始化表结构。
  • 初始化默认情绪状态。
  • 保存聊天消息。
  • 读取最近聊天消息。
  • 保存长期记忆。
  • 读取长期记忆。
  • 读取情绪状态。
  • 保存情绪状态。

还有一个细节是:Database.connect() 每次都会创建新的短生命周期连接。

这是为了避免在 UI 主线程和后台线程之间共享同一个 sqlite3.Connection。桌面程序里一旦涉及线程,共享数据库连接很容易出现奇怪问题,所以这里提前规避。

10. 长期记忆系统

长期记忆服务在:

src/aicompanion/services/memory_service.py

当前版本先采用非常简单的关键词规则。

如果用户消息中包含这些词:

我喜欢
我讨厌
记住
我的名字
我正在

就把整条用户消息保存进 memories 表。

例如用户输入:

记住,我喜欢晚上写代码。

这条消息就会被保存为长期记忆。

构建 prompt 时,会读取最近的重要记忆:

memories = self.database.recent_memories()

然后整理成:

- 记住,我喜欢晚上写代码。
- 我的名字是 ...

这些内容会放进系统提示词,让模型在回复时可以参考。

当前记忆系统还很朴素,但已经有了最小闭环:

用户说出重要信息
  -> 规则命中
  -> 保存到 SQLite
  -> 下次构建 prompt 时注入长期记忆

后续可以升级为 LLM 自动总结,或者加入向量检索。

11. 角色设定

角色设定在:

src/aicompanion/domain/character.py

目前定义了一个 CharacterProfile

@dataclass(frozen=True)
class CharacterProfile:
    name: str
    description: str
    speaking_style: str

默认角色名是:

Emilia

这里需要注意:当前代码中的设定说明是“原创陪伴角色”,并明确要求不要声称自己是受版权保护的角色。

角色设定目前会进入系统提示词:

Character name: Emilia
Character description: ...
Speaking style: ...

后续这个模块可以继续扩展:

  • Live2D 模型路径。
  • 不同情绪对应的表情。
  • 角色口癖。
  • 不同亲密度阶段的说话方式。

12. 情绪状态系统

情绪状态定义在:

src/aicompanion/domain/emotion.py

当前包含四个字段:

mood: str = "calm"
affection: int = 50
loneliness: int = 15
energy: int = 80

含义大致是:

字段 含义
mood 当前心情
affection 亲密度
loneliness 孤独感
energy 精力

当用户发送消息后:

self.loneliness = max(0, self.loneliness - 10)
self.affection = min(100, self.affection + 1)
self.energy = max(0, self.energy - 1)
self.mood = "happy" if self.affection >= 55 else "calm"

也就是:

  • 孤独感下降。
  • 亲密度略微上升。
  • 精力略微下降。
  • 亲密度到一定程度后心情变为 happy

当用户长时间没有回复时:

self.loneliness = min(100, self.loneliness + 5)
if self.loneliness >= 60:
    self.mood = "lonely"

也就是孤独感会上升,并可能切换到 lonely 状态。

这些情绪值目前已经会保存到 SQLite,并且会进入系统提示词:

Current emotion:
- mood: calm
- affection: 50
- loneliness: 15
- energy: 80

后续它可以影响:

  • 回复语气。
  • 主动消息频率。
  • Live2D 表情。
  • TTS 声音参数。

13. 主动消息雏形

主动消息服务在:

src/aicompanion/services/proactive_service.py

它使用 QTimer,每 60 秒检查一次:

self.timer.setInterval(60_000)

定时器本身不决定说什么,而是调用:

self.controller.maybe_create_proactive_message()

当前主动消息触发条件是规则式的:

  • 用户至少说过一句话。
  • 最近 30 分钟没有用户消息。
  • 最近 30 分钟没有发过主动消息。
  • 情绪状态里的 loneliness 达到一定阈值。

如果条件满足,就会生成一条固定主动消息,并保存到数据库:

你这完了吗?我刚刚有点想你。不过不用着急,我会在这里等你。

然后通过 Signal 发给窗口显示。

这个功能现在还只是雏形,但它已经证明了主动消息的基本链路:

QTimer 定时触发
  -> Controller 判断规则
  -> 更新情绪状态
  -> 生成主动消息
  -> 保存到 SQLite
  -> MainWindow 显示

后续可以把固定文本升级为 LLM 生成,让角色根据当前情绪和记忆主动说更自然的话。

14. 系统提示词构建

当前每次请求模型时,Controller 都会构建系统提示词。

系统提示词由几部分组成:

  • 角色名。
  • 角色描述。
  • 说话风格。
  • 当前情绪。
  • 长期记忆。

大致结构是:

Character name: Emilia
Character description: ...
Speaking style: ...

Current emotion:
- mood: calm
- affection: 50
- loneliness: 15
- energy: 80

Long-term memories:
- ...

这一步很关键。

因为 AICompanion 不是单纯把用户输入发给模型,而是把“角色是谁”“现在状态如何”“记得用户什么事情”一起发给模型。

这样后续的陪伴感、角色感、长期记忆和情绪变化,才有地方发挥作用。

15. 当前已经完成的功能清单

截至目前,代码里已经实现的功能可以整理为:

功能 当前状态
PySide6 桌面窗口 已实现
聊天列表显示 已实现
输入框和发送按钮 已实现
回车发送 已实现
用户/AI 消息左右区分 已实现
启动时加载历史记录 已实现
后台线程请求回复 已实现
流式 token 显示 已实现
本地 mock 模型 已实现
OpenAI-compatible API 客户端 已实现
非流式回复接口 已实现
流式回复接口 已实现
.env 配置读取 已实现
SQLite 表结构 已实现
聊天记录保存 已实现
长期记忆保存 已实现
长期记忆注入 prompt 已实现
情绪状态保存 已实现
用户消息后更新情绪 已实现
久未回复时提升孤独感 已实现
主动消息定时检查 已实现
主动消息显示 已实现
任务提醒表结构 已预留
TTS 语音回复 未实现
Live2D 展示 未实现

从这个表可以看出,项目目前已经越过了“单纯聊天窗口”的阶段,进入了“桌面 AI 陪伴系统雏形”的阶段。

16. 当前版本的不足

虽然基础流程已经打通,但现在还有不少明显不足:

  • 聊天 UI 还比较简单,没有气泡、头像和富文本。
  • 长期记忆只是关键词规则,容易漏掉重要信息。
  • 主动消息还是固定文本,不够自然。
  • 情绪系统只影响 prompt,还没有真正驱动表情或语音。
  • reminders 表已经有了,但任务提醒功能还没有完整实现。
  • API 客户端还没有做重试、取消请求和更细的错误展示。
  • 还没有 TTS。
  • 还没有 Live2D。

不过这些不足并不是坏事。现在最重要的是,项目的各个基础模块已经有了明确位置,后面可以逐步替换和增强。

17. 下一步计划

接下来我准备继续完善两个方向。

第一个方向是大模型接入体验:

  • 更详细记录 OpenAI-compatible API 的接入过程。
  • 测试真实模型回复。
  • 优化流式响应错误处理。
  • 让角色回复更符合设定。

第二个方向是本地存储和记忆:

  • 更完整地讲 SQLite 表设计。
  • 给记忆增加去重或更新逻辑。
  • 让长期记忆不只是保存原句,而是变成更适合 prompt 的摘要。
  • 后续加入任务提醒的增删查改。

到目前为止,AICompanion 已经有了一个可以继续生长的核心骨架。接下来每一步功能增强,都可以围绕这个骨架慢慢叠上去。