Files
NotesAgentic/docs/development/后台运行日志与压力问题修复.md

9.6 KiB
Raw Permalink Blame History

后台运行日志与压力问题修复

1. 范围与入口

本次针对 Agent 与任务压测发现的历史记录截断、同步数据库写入阻塞事件循环、长 Trace 树形筛选过慢进行修复,并增加统一运行日志。

主导航的「日志」打开 /logs。知识库选择页也有入口;无需成功打开 Vault 即可查看。需要 AI Core 正常提供 HTTP 服务。后台未启动时,页面显示连接错误,不伪造历史结果。

页面提供级别、模块、事件名/错误码/关联 ID 筛选及详情展开。日志列表高度受限,按时间从旧到新显示,默认每 5 秒获取新日志并跟随到底部。向上滚动时暂停跟随,抵达顶部自动加载更早日志并保持阅读位置;可一键回到最新。隐藏页面和卸载后停止轮询。界面最多保留当前浏览窗口的 500 条,继续读取旧日志时移出另一端的记录,后台保留窗口不受影响。所有组件使用已有主题变量、卡片、按钮、下拉框及 ui-disclosure 展开样式。

2. 日志覆盖

模块 记录内容 定位信息
vectors 后台索引状态、向量调用失败、是否使用本地索引回退 job_id、错误码、异常类型、调用位置、批次数量
models 本地模型/远程模型运行诊断、CUDA/CPU、失败与回退 模型、设备、状态、错误码、耗时
agent 创建运行、模型轮次、工具调用与结果、权限决策、完成/失败/取消 run_id、Provider、模型、步骤、事件序号、工具名
tasks 创建、修改、删除及不存在的删除目标 task_id、关联 note_id、修改字段名、状态
providers / chat 模型请求完成或未完整结束、流式模型错误、对话失败 Provider、模型、request_id、run_id、错误码
http / api HTTP 写操作、失败请求、超过一秒的请求及业务错误 HTTP 方法、路由模板、状态码、耗时、request_id、资源 ID
system / Python logger 服务启动与停止,以及应用、服务器和依赖库的 warning/error 模块、代码位置、异常类型

X-Request-ID 响应头可与日志中的 request_id 对照。请求上下文通过 ContextVar 传播到后台任务和线程;Agent 内部工具触发的日志还带 run_id。查询日志自身以及正常短轮询不生成 HTTP 操作日志,避免轮询放大日志量。

这里只记录操作诊断,不替代完整 Agent Trace、Token 用量台账和模型诊断表。旧代码 logger 的自由文本可能包含笔记或厂商返回,因此桥接时只记录级别、模块、代码位置和异常类型;需要业务错误码的地方使用结构化接口。

3. 存储、容量与隐私

  • 位置:APP_DATA_DIR/logs/operations.sqlite3,与业务数据库独立,应用重启后可继续查看。
  • 保留最近 20,000 条;每批写入时淘汰更早的记录。不是按天永久归档。
  • 后台线程批量写入,队列最多 4,096 条,一批最多 128 条。队列满时丢弃新诊断记录并计数,绝不阻塞保存或模型推理。
  • 日志页面显示待写入数量、队列溢出和写入失败数量。计数属于当前进程;重启后重置。存储不可读时显示请求失败。
  • 正常退出先停止 Agent 和其他后台工作,再排空日志队列;强制终止进程可能丢失尚未写入的队列内容。
  • 不记录请求/响应正文、系统提示词、工具参数/输出、笔记标题或正文、凭据、原始异常消息、查询字符串。元数据按白名单筛选并限长,Bearer 和 sk- 凭据形式额外脱敏。
  • API 仅返回保留窗口内的诊断数据,不提供任意文件读取和日志删除接口。

4. 开发接口

from app.operation_logs import log_event

log_event('vectors', 'embedding.failed', level='ERROR', error=exc,
          model=model_id, job_id=job_id, error_code='EMBEDDING_UNAVAILABLE',
          fallback='local_index')

第一个参数是模块名。source 是可选的元数据字段,例如 localapi,不要与模块名混淆。事件名应使用开发时定义的短常量。不要把正文、URL、异常消息或用户输入拼入事件名。

GET /api/logs 参数:

参数 规则
limit 默认 50,范围 1200
before 上页 next_cursor;正整数,返回更早的 ID
level 空或 INFO / WARNING / ERROR / CRITICAL
source 模块名精确匹配,最多 100 字符
q 在事件名及脱敏元数据中做字面子串搜索,最多 200 字符

返回 itemsnext_cursorsourcespendingdroppedwrite_failuresretention。按递增 ID 的倒序分页,插入新日志不改变已经翻到的旧页边界。没有匹配记录时 items 为空、next_cursor 为 null。

5. 压力问题的修复方式

Agent Trace 写入由独立异步队列调度到线程,最多合并 64 个写入为一个 SQLite 事务。提交成功后才更新对外可见快照和广播 SSE。创建记录在第一次 await 前占用运行名额,避免并发创建突破 200 个活动运行上限。取消等待正在提交的快照结束,再写终态;服务关闭会等待 Agent 和 Trace 队列。数据库失败不会被当成成功提交。

任务 HTTP 写操作在协程中排队,然后在线程执行;读操作也在线程执行,减少文件锁争用和主事件循环阻塞。SQLite 仍是单写者,不意味着任务写入支持无限并发。

前端读取任务和 Agent 列表的后续 API 页,不再只显示第一页的 50 条。任务筛选和数量基于已获取的完整列表,界面每页显示 100 个任务;运行侧栏每页 50 条。列表刷新失败保留上次结果。列表读取使用现有 offset 协议,不是跨多页的事务快照;其他客户端同时修改数据时可以刷新重新获取。

Trace 搜索先生成事件序号、模型调用 ID、工具调用 ID 的集合,再一次遍历调用树。匹配子节点保留父节点。搜索作用于完整已加载的历史,每页只渲染 200 个事件或树节点。工具统计按工具名和状态汇总,避免在底部再次生成数千个调用卡片。

6. 验证方法

后端回归覆盖日志持久化、重启、保留窗口、筛选与游标、正文排除、HTTP 关联 ID,以及后台 Trace 写入同时取消时仍释放等待方。业务回归覆盖 Agent/任务、模型路由、本地模型、用量统计和对话。

cd backend
.venv/Scripts/python.exe -m pytest tests/test_agent_core.py tests/test_api.py tests/test_operation_logs.py tests/test_model_routing.py tests/test_local_models.py tests/test_usage_overrides.py tests/test_chat_history.py tests/test_chat_context.py -q

运行测试前应将 APP_DATA_DIR、APP_DB_PATH 和 APP_VAULT_PATH 指向临时目录,避免模块导入时初始化真实扩展。pytest 的 fixture 会继续为每个测试隔离数据。

前端执行 npm --prefix frontend testnpm --prefix frontend run build。新增用例验证后续任务/Agent 页完整加载、Trace 全历史搜索与分页、日志筛选/游标、自动跟随与上滚暂停、顶部历史加载、存储异常提示及卸载后停止刷新。

2026-09-06 实测:后端全量 627 项通过;随后增加的 3 项写入失败/取消回归也通过,最后相关接口回归 21 项通过。前端全量 413 项通过,生产构建通过。构建仍提示已有部分 Markdown/Mermaid 依赖块大于 500 kB;后端测试保留已有 Starlette TestClient 弃用提示。纸间时光日志页已用隔离合成数据检查截图,运行中的本机 /api/logs 也已确认可返回结果。

离线并发压测与真实本机 HTTP 压测复现方式、前后数据见 Agent 与任务压测报告。压测只使用隔离数据和 Mock Provider,不消耗用户厂商额度。

手工验收建议:

  1. 创建、修改和删除一个测试任务,按返回的 task_id 或 X-Request-ID 筛选日志。
  2. 运行含工具调用的 Agent,批准或取消权限请求;按 run_id 检查创建、权限、工具结果和终态。
  3. 在隔离配置中请求未安装的本地模型,查看 models/vectors 的错误码与回退信息,不应出现输入文本。
  4. 重启 AI Core,确认已提交日志仍可查看;退出 Vault 后从入口页打开日志。
  5. 切换默认浅色、深色、纸间时光主题,检查筛选栏、错误徽标和详情展开;窄窗口下应换行且详情不撑出页面。

2026-09-06:大批量本地向量传输修复

全量重建包含 2111 个文本块时,本地模型可能成功完成推理,但整批向量被写成一行 JSON。接收端单行限制为 16 MiBasyncio 管道拒绝超长行,Windows 线程管道则读到不完整 JSON,最终被包装成 LOCAL_MODEL_INVALID_RESPONSE。这类错误不表示 CUDA 显存不足,也不应通过切换 CPU 处理。

Embedding 响应改为每 128 条向量一个 JSON 帧,结束帧携带总数。接收端检查偏移连续性、请求数量和结束标记;缺块时明确失败,不把部分向量写入索引。模型仍只加载一次,推理批大小不变。语音等其他响应保持原协议。

验证方法:在隔离数据目录运行 tests/test_local_models.py,分别使用 asyncio 与 Windows 线程管道传输 2111 × 384 的结果,确认旧格式超过 16 MiB,而分帧可完整接收。该文件 7 项测试通过。另使用本机 Bekko 权重、CUDA 对 2111 条合成文本做真实推理:返回 2111 条 384 维向量,设备 cuda:0,耗时约 12.55 秒;旧格式 JSON 为 17,886,180 字节。该验收不修改用户笔记、索引或运行配置;用户原始长文档的全库重建仍需在服务加载修复后重试。