上一篇日志整理了 AICompanion 当前已经实现的功能:PySide6 聊天窗口、SQLite 本地存储、长期记忆、情绪状态、主动消息雏形,以及 OpenAI-compatible 大模型接口。

写完之后,我继续往前推进了一小步。我开始把项目从本地 mock 回复,推进到更稳定地接入真实大模型。

同时我也重新校准了一下项目定位。AICompanion 不是要做成一个传统桌面助手,不是以任务管理、效率工具或系统控制为核心。它最终想实现的是一个基于 Live2D 动漫角色的情感交互应用。也就是说,后续所有模块都应该服务于“角色感”“陪伴感”“情绪反馈”和“长期关系感”。

1. 这次新增内容概览

这次主要新增和完善的是大模型接入体验,集中在 llm 模块和 README 文档中。

当前相关文件包括:

src/aicompanion/llm/
  base.py
  mock_client.py
  openai_compatible_client.py
  factory.py
  diagnostics.py

其中这次比较重要的是:

src/aicompanion/llm/diagnostics.py
src/aicompanion/llm/openai_compatible_client.py
README.md

也就是说,现在项目不只是“代码里有一个真实模型客户端”,而是有了一个单独的诊断入口,可以在不启动桌面窗口的情况下,先测试模型配置是否正确。

这对后续开发很重要。因为如果每次测试模型都要打开 GUI,再输入消息,再看窗口里是否报错,调试效率会很低。现在可以先在命令行里确认模型连通性,再回到 PySide6 界面里测试完整交互流程。

2. 新增 LLM 诊断命令

新增的诊断工具位于:

src/aicompanion/llm/diagnostics.py

它的作用是:在不打开桌面 UI 的情况下,直接读取 .env 配置,创建当前配置对应的 LLM 客户端,然后发送一条测试消息。

README 中也补充了使用方式:

.\.venv\Scripts\python.exe -m aicompanion.llm.diagnostics

如果想发送自定义测试消息,可以这样写:

.\.venv\Scripts\python.exe -m aicompanion.llm.diagnostics "你好,请简单介绍一下你自己"

默认情况下,诊断命令会测试流式输出。也就是模型返回的内容会一段一段打印出来,模拟正式聊天窗口里的打字效果。

如果想测试非流式回复,可以加上:

.\.venv\Scripts\python.exe -m aicompanion.llm.diagnostics --no-stream

这个工具的价值不在于功能复杂,而在于它把“模型接入是否正常”从 GUI 流程里拆了出来。后续无论换 OpenAI-compatible 平台、换模型名、换 base_url,还是调整 API key,都可以先用这个命令做一次最小验证。

3. 诊断工具的执行流程

diagnostics.py 的流程很简单:

读取命令行参数
  -> 加载 .env 配置
  -> 打印 provider / base_url / model / streaming
  -> create_llm_client(settings)
  -> 构造一条测试消息
  -> 调用 generate_reply_stream() 或 generate_reply()
  -> 打印模型回复

核心逻辑大致是:

settings = load_settings()
client = create_llm_client(settings)

messages = [ChatTurn(role="user", content=args.message)]

for token in client.generate_reply_stream(
    messages=messages,
    system_prompt=system_prompt,
):
    print(token, end="", flush=True)

这里没有复用正式聊天窗口里的完整角色 prompt,而是写了一个更小的诊断 prompt:

You are a warm AI companion. Reply naturally in Chinese, and keep the answer concise.

这样做是为了让诊断工具保持轻量。它主要验证 API 是否能通、流式解析是否正常、模型是否能返回中文内容,而不是完整测试角色人格。

完整角色设定仍然在 ChatController._build_system_prompt() 中负责构建。

4. 流式和非流式都可以单独测试

当前项目的 LLM 抽象接口本来就同时支持两种模式:

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

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

桌面窗口默认使用流式回复,因为这样更像角色正在一句一句说话,而不是突然弹出一整段文本。

但是非流式接口仍然有保留价值:

  • 方便快速测试模型是否可用。
  • 方便后续写自动化测试。
  • 适合某些不支持流式输出的平台。
  • 后续如果做“记忆摘要”“情绪分析”等内部任务,非流式会更简单。

因此这次诊断工具同时支持两种模式。这样后续排查问题时,可以判断问题到底出在普通 API 调用,还是出在 SSE 流式解析。

5. API 错误处理进一步清晰化

真实 API 接入和 mock 最大的区别是:真实 API 会失败,而且失败原因很多。

可能是:

  • API key 没填。
  • API key 错误或过期。
  • base_url 写错。
  • 模型名不存在。
  • 平台余额不足。
  • 请求被限流。
  • 网络或代理异常。
  • 平台返回的流式数据格式不符合预期。

如果把这些底层异常直接抛给 UI,用户看到的可能是 urllib.error.HTTPErrorURLErrorJSONDecodeError 之类的信息,既不友好,也不利于判断下一步该改哪里。

所以 openai_compatible_client.py 中增加了统一异常:

class LLMResponseError(RuntimeError):
    ...

它的目的不是隐藏错误,而是把底层错误整理成更容易理解的提示。

例如 HTTP 状态码会被转换成更明确的排查方向:

状态码 提示方向
401 检查 API key 是否正确
403 检查账号权限、模型权限或访问限制
404 检查 base_url 或接口路径
429 检查限流、余额或配额
500+ 平台服务端可能异常

这样当桌面窗口请求失败时,UI 显示的就不是一大串 traceback,而是相对可读的错误说明。

6. 流式响应解析的处理

OpenAI-compatible 的流式输出通常是 SSE 格式,大致长这样:

data: {"choices":[{"delta":{"content":"你好"}}]}
data: {"choices":[{"delta":{"content":"呀"}}]}
data: [DONE]

当前客户端会逐行读取响应:

for raw_line in response:
    line = raw_line.decode("utf-8", errors="ignore").strip()

然后只处理 data: 开头的内容。

如果遇到:

data: [DONE]

就结束本次流式响应。

如果是普通 JSON 事件,就从里面取:

choices[0].delta.content

这个过程现在已经被封装在 _parse_stream_token() 中。这样后续如果某个平台的兼容格式有细微差异,也可以集中在这里调整。

7. 这一步和情感交互有什么关系

表面上看,这次新增的是命令行诊断、错误处理和 API 调试工具,好像和“情感”没什么关系。

但对 AICompanion 来说,这其实是情感交互的地基。

因为一个情感交互角色至少需要稳定做到几件事:

  • 能持续对话。
  • 能自然地逐段回复。
  • 出错时不让整个体验突然崩掉。
  • 能根据角色设定、情绪状态和长期记忆来组织语言。
  • 后续能把文本回复同步给 Live2D 表情、动作和 TTS。

如果模型调用本身不稳定,后面的 Live2D、语音、表情、记忆都会被影响。

所以当前阶段不是在做“桌面助手能力”,而是在补齐一个情感角色应用必须有的对话基础设施。

换句话说,现在的目标不是让 AICompanion 帮我完成多少任务,而是让她能更稳定、更自然、更有角色感地回应我。

8. 项目方向重新校准

之前代码中已经预留了 reminders 表,也有主动消息雏形。但这不代表项目要走传统助手路线。

我现在更明确地把 AICompanion 的方向定为:

Live2D 动漫角色 + 大模型对话 + 长期记忆 + 情绪状态 + 语音与动作反馈

它更接近一个情感交互应用,而不是效率工具。

因此后续功能排序也应该围绕“情绪反馈”和“角色表现”展开。

例如,长期记忆不只是为了记住任务,而是为了让角色形成关系连续性:

  • 记得用户喜欢什么。
  • 记得用户讨厌什么。
  • 记得用户最近在做什么。
  • 记得用户常见的情绪状态。
  • 在合适的时候自然提起,而不是机械复述。

情绪系统也不只是 prompt 里的几个数字,而应该逐步驱动:

  • 回复语气。
  • 主动关心的频率。
  • Live2D 表情。
  • Live2D 动作。
  • TTS 的语速、音量和情绪。

这才是项目后续真正要生长的方向。

9. 当前已经推进到什么程度

结合上一篇日志和这次新增内容,现在项目状态可以更新为:

模块 当前状态
PySide6 聊天窗口 已实现基础版本
后台线程流式回复 已实现
本地 mock 模型 已实现
OpenAI-compatible 客户端 已实现
LLM 命令行诊断工具 已新增
流式/非流式模型测试 已新增
API 错误提示整理 已增强
SQLite 聊天记录 已实现
长期记忆 仍是关键词规则
情绪状态 已有基础数值模型
主动消息 仍是规则和固定文本
Live2D 未实现
TTS 未实现

也就是说,这次没有大幅增加新的用户可见功能,但增强了真实模型接入的可调试性和可靠性。

这一步完成之后,后面继续做情感层能力会更踏实。

10. 下一步计划

接下来我准备把重点放回“情感交互”本身,而不是扩展成桌面助手。

更适合下一阶段的方向是:

  1. 优化角色 prompt,让回复更稳定地符合 Emilia 的性格设定。
  2. 改造长期记忆,不再只保存原句,而是提取成适合角色理解的摘要。
  3. 设计情绪状态到角色表现的映射,比如 happylonelycalm 分别对应什么表情和语气。
  4. 为 Live2D 接入提前设计角色状态接口,而不是一上来就把模型显示出来。
  5. 后续再接入 TTS,让文本回复逐步变成“说话”。

对 AICompanion 来说,Live2D 不是单独的展示层,而应该是情感状态的外显。

所以在真正接入 Live2D 之前,我需要先让内部状态更清楚:角色现在是什么心情,为什么变成这个心情,她应该用什么语气回应,又应该用什么表情和动作表现出来。

这一篇日志记录的就是这个过渡阶段:从“能聊天”走向“更稳定地接入真实模型”,再继续朝“能形成情感反馈的角色”前进。