罗德岛通讯与技术部

故障知识库

TROUBLESHOOTING

tail -30 /opt/rhodes-island/data/logs/error.log | grep -A5 "Traceback"

状态:🟢 现行 · 最后核对 2026-08-28

症状 → 根因 → 修复。每次修完 bug 加一条。Ctrl+Shift+F 搜症状关键词快速定位。


服务崩溃

服务循环重启(activating auto-restart, status=1)

症状systemctl status rhodes 显示 activating (auto-restart),重启计数器持续增长

排查

tail -30 /opt/rhodes-island/data/logs/error.log | grep -A5 "Traceback"
python -c "from app.main import app"  # 直接测 import

常见根因

根因 日志关键词 修复
权限问题 PermissionError: .../data/logs/ chown -R root:root /opt/rhodes-island/data/
缺少 import NameError: name 'Path' is not defined from pathlib import Path
嵌入模型下载失败 OSError: BAAI/bge-m3 + Network unreachable 切回 MiniLM 或设 HF_ENDPOINT
.env 覆盖默认值 模型加载了 MiniLM 而不是 BGE 检查 .env 里的 LOCAL_EMBEDDING_MODEL

修复后验证

sudo systemctl restart rhodes && sleep 15
sudo systemctl status rhodes | grep "Active:"
# 应该显示 active (running),不出现 restart counter

git pull 说「本地改动会被覆盖」(2026-08-05)

症状

error: Your local changes to the following files would be overwritten by merge:
        data/<某个运行时文件>.json

根因不是「谁改了那个文件」,是它一开始就不该被跟踪。

⚠️ .gitignoredata/ 不是整目录忽略的,是逐个文件列的 —— 所以往 data/ 下新加一个应用会写的文件时,默认就是被跟踪的, 而且不会有任何提示。git add -A 会把它连同里面的运行时数据一起提交。

分两种情况,修法完全不同

这个文件在仓库里有用吗 修法
没用(纯运行时状态,如 data/key_health.json 加进 .gitignore + git rm --cached
有用(提供初值,如 data/runtime_config.json git update-index --skip-worktree <文件>

⚠️ --skip-worktree只存在于本机 .git的设置,不在版本库中 —— 换台机器、重新 clone 就没有了。所以它是 deploy/setup.sh 的第 7 步, 不能靠人记得。查它有没有生效:

git ls-files -v data/runtime_config.json    # 开头是 S 就对了

⚠️ 代价:仓库里对那个文件的合法更新也不会再应用到这台机器。 对「服务器运行时状态」这个取舍是对的,但改了仓库里那份之后 别指望它自己生效。


改了但没生效(2026-07-29/30 新增,这类占比最高)

症状:代码改对了、部署了、日志零报错,但功能就是没变化。

按这个顺序查,不要跳步(跳步的代价见下面每条的实例):

# 查什么 命令 踩过的实例
1 生产目录是哪个 systemctl cat rhodes | grep WorkingDirectory 服务器上同时有 ~/rhodes-island(停在 master 的旧 clone)和 /opt/rhodes-island,差点在错目录打完补丁宣布修好
2 代码真在里面吗 该目录 git log -1 + grep 关键函数
3 服务重启过吗 systemctl show rhodes -p ActiveEnterTimestamp --value
4 轮到传输层(且先看配置再改代码) 见下条 深聊不流式——原因至今未定,别照抄结论

深聊/聊天不是逐字出现(明明后端在流式)

症状:等十几秒,整段一次性蹦出来。后端逐 token yield、前端 getReader() 增量渲染, 两边代码都对,日志一个 ERROR 都没有

根因:🔴 未定。2026-07-30 复核推翻了原先的结论,见下。

先按这个顺序查(顺序本身是重点):

  1. 服务重启过吗 —— systemctl show rhodes -p ActiveEnterTimestamp --value。 07-29 那次最可能就是这一条:补丁先打在陈旧 clone ~/rhodes-island 上, 后来才打进真正在跑的 /opt/rhodes-island 并重启,于是分支里早就有的 流式改动才上线。
  2. 前端真的在增量渲染吗 —— getReader() 那个循环里加一行 console.log, 看是逐块到还是一次到。这一步能把问题一刀切在前后端之间,最该先做。
  3. 轮到传输层。而且要先看配置再改代码sudo nginx -T | grep -n "proxy_buffering\|gzip"

现有措施:SSE 端点统一带 core.stream_filters.SSE_HEADERS (含 X-Accel-Buffering: no)。保留是因为无害、且换代理/加 CDN 时真需要。

⚠️ 原先这里写的是「nginx 默认 proxy_buffering on 把分块攒起来」,那是错的。 实测生产 nginx 反代 8001 的 location /本来就有 proxy_buffering offgzip ongzip_types 是注释状态,默认只压 text/html,不含 text/event-stream; 应用层也没有 GZipMiddleware。三个缓冲点全排除,那个头是 no-op。

同时作废的还有一句旁证推理:「群聊回复短、填不满缓冲区,所以群聊流式正常是假象」—— 那是为了自圆其说而编的,缓冲区根本没参与。

教训:一个说得通的机制 + 一次成功的观察,不等于因果。 这和「验证手段选错比没验证更糟」是同一类错误的两个方向。

日志里查不到某条错误,但问题确实存在

根因本项目的应用日志走 loguru 写 data/logs/rhodes_YYYY-MM-DD.log,不走 stdout。 journalctl -u rhodes 里永远看不到它们。

2026-07-30 因为这个得到过假阴性:怀疑对了元凶,用 journalctl -f | grep 验证得到"不跳", 据此把真元凶排除了,然后带着假前提绕了三轮。验证手段选错比没验证更糟。

⚠️ 反过来也要注意:pytest 和生产写同一个日志文件。日志里的错误可能是测试产生的 (test_op / test_operator 是测试干员名)。按时间戳和相邻行判断来源。

定位来源的通用招式

grep -n -B8 "<错误关键字>" data/logs/rhodes_$(date +%F).log | tail -30

错误的节律本身就是信息:

形状 指向
固定间隔(每 60s / 每 2h) 后台任务(agent_loop / narrative.scheduler
紧跟某条 INFO(如 chat_endpoint:787 那个请求路径
只在启动附近、按字母序铺开 启动钩子遍历全部干员

用户数据串号 / 读到别人的数据

关系视图里的信赖数字不是自己的

根因RelationGraph 是进程级全局单例,_doctor_edges 曾只用 op_id 做键—— 所有用户共用一份。谁最后和某个干员聊过,谁的数字就是所有人看到的那份。 2026-07-30 已修(键改 {username}:{op_id})。

同类风险:任何进程级单例里存用户数据的字典,键必须含 username。

干员情绪状态 / 世界观上下文像是别人的

根因request_username 是 ContextVar,worker 线程不继承它。 往 ThreadPoolExecutor 裸提交靠它定位用户的调用,会静默读写 _no_user_ 那份公共桶。

判定:日志里每次调用刷一条 request_username 为空,降级为 _no_user_

修复submit(contextvars.copy_context().run, fn, arg)每个任务各自一份快照(共用一个 Context 并发进入会抛 RuntimeError: cannot enter context)。

data/users/ 下出现没人认识的"用户"

根因_no_user_ / __no_user__ 是降级目录,但它们是目录, 裸 iterdir() 列用户会把它们当成真用户——心跳为它们跳动、主动消息为它们生成、L1 为它们累积。 一旦生成出 *_somatic.json 就自我喂养、永不停止。

修复:列用户只用 paths.list_real_users()。已攒的数据用 scripts/archive_pseudo_users.py 挪到 data/_orphans/

前端问题

主题换了,但某块区域还是旧主题的颜色

先查那块地方是不是根本不吃 token。 2026-07-31 用户报「纸和铜锈最上面和最下面那块 是黑的」,根因不是某处写错值 —— 顶栏 / Tab 栏 / 弹窗这一族当时写死 rgba(30,30,30,.82),浅色化靠一串 [data-theme="light"] #tab-bar { … } 手写选择器, 加新浅色主题时没往里补名字。

# 主题块之外还有哪些颜色字面量
grep -n "rgba\?(\|#[0-9a-fA-F]\{6\}" frontend/css/style.css | grep -v "^\s*--"

再查是不是 JS 用内联样式盖掉了。 同一天还栽过一次:整套 --chat-ground token 做完、 「夕」也设了 none,聊天页照旧铺甲板照片 —— 因为 main.js_applySceneBackground() 在 CSS 之外又用 view.style.backgroundImage 写了一遍,内联优先级高于任何 CSS 规则。 当时只在 CSS 里搜了 backgrounds/,没搜 JS。

grep -rn "\.style\.background\|\.style\.color" frontend/js/*.js

第三种:主题块漏了某个 token → 静默落回 :root 的深色值。跑 pytest tests/test_theme_polarity.py 就能查出来。


批量改完 JS,语法检查过了但行为不对

node --check 挡不住注释被破坏。 2026-07-31 的批量脚本往一行 // 注释中间塞了 多行内容,// 只管到第一行,后面几行变成了真代码:

// …但也不能像原来那样 .catch(() => {
    // apiFetch 内部已经…
}) 全吞:

语法检查照样通过(残骸恰好仍是合法 JS)。可靠的查法是剥掉注释只比对真代码行

# 剥注释后 diff 改动前后,任何"注释变代码"都会显形
# 完整实现见 tests/test_no_silent_catch.py 里的 _comment_mask

根因是判断「某处是不是注释」,正则和朴素扫描都不够。同一天栽了两次: avatarFileInput.accept = 'image/*' 里的 /* 被当成块注释开头(那行之后所有代码被判成 注释,9 处真代码被静默跳过);.replace(/[&<>"']/g, …) 这个正则字面量里的引号被当成 字符串开合(后面的注释不再被识别)。要可靠区分,扫描器必须同时处理字符串、模板串 和正则字面量。


改了前端,自己刷新看得到,别人看不到

按这个顺序查:

  1. 跑过 python scripts/bump_frontend_cache.py 没有? 相对 import 不继承父模块 URL 上的查询串 —— main.js?v=xxx 管不到 ./settings.js
  2. nginx 有没有覆盖 Cache-Control?
    curl -sI https://manana.icu/js/main.js | grep -i cache-control
    curl -sI http://127.0.0.1:8001/js/main.js | grep -i cache-control   # 源站
    
    两边不一样就是 nginx 在改。/js/ /css/ 应该是 no-cache, must-revalidate
  3. 那个文件是 nginx 直接发的还是代理出去的? /js/ /css/ 用的是 alias(从磁盘直发,不经过 FastAPI),所以 app/main.py 的中间件对它们无效

页面白屏

症状:刷新后完全空白,Console 有红色 JS 错误

常见根因

根因 修复
operatorConfig[opId] 为 undefined 加 `
IndexedDB 读取失败 清浏览器缓存 → 硬刷新
旧 JS 缓存 Ctrl+Shift+R 硬刷新,或改 main.js?v=0102 版本号

AI 回复只显示 "...."

症状:气泡内容只有 3-4 个点

排查流程

  1. 查服务日志 grep "LLM chat" data/logs/ | tail -5——看回复 token 数
  2. 如果 1536tk → 不是后端问题,是前端 SSE 中断
  3. 如果 1-2tk → 后端真的回了省略号

常见根因

根因 修复
限流 429 被当 SSE 解析 前端加 429 处理 + toast 提示。见 CHANGELOG 2026-06-26 条目
移动端 fetch 断连 深聊长回复 8s SSE 流被移动网络截断。加重试按钮(未修,技术债务 #5)
[继续] 指令触发 深聊空输入自动发 [继续]——LLM 可能输出 ……

聊过的干员点进去就重加载

症状:有聊天记录的干员一点就刷新页面,没聊过的正常

根因operatorConfig[opId] 为 undefined——共享源 operator-config.jsmain.js 内联回退不一致

修复:所有 operatorConfig[id] 访问加 || {} 兜底。见 668e748


记忆与检索

Embedding 维度不匹配

症状

ValueError: shapes (3,384) and (512,) not aligned
WARNING: L4 检索失败: Collection expecting embedding with dimension of 384, got 1024

根因:切换嵌入模型后,旧数据(SQLite/ChromaDB)还是旧维度

修复

位置 修复
deep_chat detail_index 过滤 len(emb) != len(query) 的旧条目
L4 ChromaDB catch dimension error → 静默跳过
L3 旧集合 双集合过渡——新事实写 BGE,旧集合只读降权。查询时 catch 维度错误

预防:切换嵌入模型前先备份 ChromaDB 目录

启动报 KeyError: '_type'(chromadb 版本与数据格式不匹配)

症状python -c "from app.main import app" 崩在 l4_world.py 的模块级单例上:

File "chromadb/api/configuration.py", line 209, in from_json
    f"...from JSON with type {json_map['_type']}"
KeyError: '_type'

根因:装的 chromadb 版本和写数据的版本对不上。config_json_str'{}' 时 0.5.x 的解析器会 KeyError,而 1.x 对默认配置写的正是 '{}'(真配置在 schema_str)。

⚠️ 先判断方向:是该升还是该降?

看 SQLite 的 schema,不要靠猜

python3 -c "
import sqlite3
c=sqlite3.connect('file:data/users/Doctor/chroma_l3/chroma.sqlite3?mode=ro',uri=True)
print('列  :', [r[1] for r in c.execute('PRAGMA table_info(collections)')])
print('迁移:', [r[0] for r in c.execute(\"SELECT filename FROM migrations WHERE dir='sysdb' ORDER BY filename\")])
c.close()"
看到什么 说明 怎么办
schema_str 列 / sysdb 迁移到 00010-collection-schema 数据由 1.x 写的 升级 pip install --upgrade "chromadb>=1.0"
只有 config_json_str、无 schema_str 数据由 0.5.x 或更早写的 装对应的旧版

判断依据:数据库里如果存在当前版本不认识的列或迁移记录,说明写它的版本更新 —— 必须往上走。

2026-07-29 的真实案例:生产环境跑的是 1.x,requirements.txt 被改成 chromadb>=0.4.22,<0.6.0 后 pip 静默降级到 0.5.23,17 个 collection 全部读不出来。 当时第一反应是"数据是老格式,该降级"——方向正好反了,靠 schema_str 这一列才纠正过来。

数据没坏:报错发生在读配置阶段,向量本身完好。别删库。

预防requirements.txt 里 chromadb 必须精确钉死==X.Y.Z), 改这一行之前先在服务器上 pip show chromadb 拿到实际版本。

ChromaDB 写入静默失败

症状:健康仪表盘 BGE 集合计数不增长,但无 ERROR 日志

排查

python -c "
from app.systems.memory.l3_long import l3_long_memory
print(f'BGE: {l3_long_memory.bge_collection.count()} 条')
print(f'旧: {l3_long_memory.collection.count()} 条')
"

根因l3_long.py 多处 except: pass 静默吞错——写入失败完全不可见

修复:在 add_facts/add_wiki_fact 的 except 块加 logger.warning。见 tech-debt.md M3


性能问题

用户感知延迟 > 20 秒

排查

grep "LLM chat" data/logs/ | tail -10  # LLM 实际耗时
grep "embed" data/logs/ | tail -5       # Embedding 耗时

常见根因

根因 表现 修复
BGE-M3 在 CPU 上 单次 embed 3-5s,每轮 3 次→12s 切 BGE-small(9ms)或 FP16
本地 qwen 内存满 Swap 100%,所有请求卡 禁用 qwen,后端走 deepseek
LLM API key 失效 401 error,用户请求失败 换 key

Swap 打到 100%

症状free -m 显示 swap 满,服务响应极慢

根因:qwen2.5-1.5b 在 3.7GB 内存上跑不了

修复systemctl stop llama-server && systemctl disable llama-server

预防:cron 每 5 分钟检查 swap,超 80% 告警。见审计报告基础设施部分


⚠️ 查日志之前先看这一节(2026-08-05 一天内栽了三次)

journalctl 看不到应用日志

deploy/rhodes.service 里写着:

StandardOutput=append:/opt/rhodes-island/data/logs/server.log
StandardError=append:/opt/rhodes-island/data/logs/error.log

应用的全部输出都被重定向进文件了,journald 一个字都拿不到。 journalctl -u rhodes 只有 systemd 自己的事件(启动、停止、SIGKILL)。

于是 journalctl -u rhodes | grep 某个应用日志 恒为空 —— 而那看起来完全像「没有这个错误」。别拿它的空当结论。

查应用日志一律用 data/logs/rhodes_$(date +%F).log

② 有两个长得像 error.log 的文件,来源完全不同

文件 谁写的 内容
data/logs/error.log systemd 重定向的 stderr loguru 控制台 sink 的全部输出(带 ANSI 颜色码)
data/logs/error_<日期>.log loguru 自己的 ERROR 级 sink 只有 ERROR 及以上

挑错一个就会得出「什么都没有」。

③ ⚠️ rhodes_<日期>.log 只是当前那个分片

日志是 rotation="10 MB" + compression="zip":写满就轮转成 rhodes_<日期>.<时间戳>.log.zip,当前那个文件里只剩最近一小段。

一天的绝大部分历史在 .zip 里,grep 直接搜不到。

这条骗过的不只是人:scripts/llm_health.py 曾因此把「一天里的一小片」 报成全天统计 —— 分子分母都对,只是样本悄悄变成了尾部一段, 而它看起来是全量(已修,2026-08-05)。

搜全天要带上轮转的:

cd /opt/rhodes-island
# 未压缩的分片
grep -h "你要找的" data/logs/rhodes_$(date +%F)*.log 2>/dev/null
# 连 .zip 一起(zgrep 对 zip 不管用,得先解)
for z in data/logs/rhodes_$(date +%F)*.zip; do
  [ -e "$z" ] && unzip -p "$z" | grep "你要找的"
done

常见命令速查

# 服务状态
systemctl status rhodes | grep -E "Active:|Main PID|counter:"

# 最近错误
grep "ERROR" data/logs/rhodes_$(date +%Y-%m-%d).log | tail -20

# LLM 延迟采样
grep "LLM chat" data/logs/rhodes_$(date +%Y-%m-%d).log | tail -10

# 内存
free -m && grep "swap\|mem" data/logs/monitor.log 2>/dev/null | tail -5

# 磁盘
df -h /opt

# 健康仪表盘
curl -s http://localhost:8001/api/memory/health/dashboard | python3 -m json.tool 2>/dev/null | grep -E "embedding_health|avg_latency|error_rate|recall|bge_facts"

Windows 上压缩 zip 别用 tar -a(2026-08-28)

症状tar -a -c -f x.zip a.txt b.txt 生成的 .zip 双击无法解压、 资源管理器报「文件为空/损坏」;python -c "zipfile.ZipFile(...)"BadZipFile

根因:git bash 的 tarGNU tar,它的 -a--auto-compress)只认 gzip/bzip2/xz 等压缩格式,不认 .zip 扩展名——于是退化成普通 tar 归档, 只是文件名骗人叫 .zip。用 file x.zip 一看就知道:显示 POSIX tar archive (GNU) 而不是 Zip archive data

修复:Windows 上生成 zip 用下面任一种,别用 tar -a

  • Python(中文文件名最稳,自动 UTF-8 flag): zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) + z.write(src, arcname=name)
  • PowerShell:Compress-Archive -Path ... -DestinationPath x.zip

相关文档:CHANGELOG · tech-debt · smoke-test

(原先链接的 engineering-logarchitecture-audit-2026-06-27 两份文档已不存在—— 前者已并入 CHANGELOG.md,后者已并入 tech-debt.md。)