我花一下午修了 7 个 bug:一个 Electron AI Agent 发版前夜的 debug 长征

0 阅读14分钟

我花一下午修了 7 个 bug:一个 Electron AI Agent 发版前夜的 debug 长征

Pi SDK 回滚、Brain 非流式困局、Intel Mac 死机、工具名幻觉、repetition_penalty 副作用 —— 给独立开发者的 5 小时真实复盘

昨天下午,我给自己独立开发的 AI 桌面应用 Lynn 发版 v0.76.2。

一个我以为 30 分钟能搞定的"打包 + 公证 + 上传 GitHub"的例行操作,结果修了 5 小时,踩了 7 个坑。

每个坑拎出来都能单写一篇技术文 —— Pi SDK 升级兼容、Electron 打包架构陷阱、LLM 工具调用幻觉、vllm 参数副作用、甚至 Vite 的 import.meta.url 冷知识。

这篇是复盘, 所有 bug 的定位过程、错误的修复思路、最后真正有效的方案,我都原样写出来。 如果你在做类似的产品,这里的每一个坑你迟早会踩。

[Lynn v0.76.2 主界面]

截屏2026-04-19 00.50.48.png


背景:Lynn 是什么

先花 30 秒让你知道在看什么:

Lynn 是一个本地运行的 AI 桌面助手,方向跟 Claude Code / Cursor 不完全一样:

  • 长期记忆:15 模块 PKM 系统(FTS5 + 向量召回 + 关系图)
  • 任务模式:9 种场景一键切换(小说 / 代码 / 商务 / 翻译 / ...)
  • 本地 GPU + 云端降级:4090 跑 Qwen3.5-32B,配合远程 Brain 服务做 6 级自动 fallback
  • 多 Agent 协作:每个 agent 独立人格 + 独立记忆 + Cron 心跳

lynn-screenshot-1776502832093.png 架构大概长这样:

Electron UI (React 19)
    ↓ WebSocket
本机 Lynn Server (Hono + Pi SDK)
    ↓ OpenAI-compat HTTPS
Remote Brain (Node.js on 腾讯云)
    ↓ 自动路由
┌─ T1 本地 GPU: Qwen3.5-32B AWQ 4bit (vllm)
├─ T2 Z.AI glm-5-turbo
├─ T3 Kimi K2.5
├─ T4 GLM-4.7
├─ T5 DeepSeek / Step Text
└─ T6 Minimax (最终兜底)

技术栈速览(给好奇源码的同学)

🏗 Lynn Desktop v0.76.2 · 完整技术架构

最后更新:2026-04-18 适用发布版本:v0.76.2(DMG / EXE)


🖥 桌面壳

技术版本
运行时Electron38
进程Main + Renderer + Preload

🎨 前端(Renderer)

组件版本用途
React19.2.4UI 框架
Zustand5.0.11状态管理(slice 架构)
CSS Modules按组件 scope 样式
Vite7.3.1构建工具
CodeMirror6代码 / MD 编辑器
Mermaid11图表渲染
KaTeX0.16数学公式

🔌 服务端(独立 Node.js 进程)

组件版本用途
Hono4.12.9HTTP router
@hono/node-server1.19Bridge 到 Node
@hono/node-ws1.3WebSocket 流
Routes24 个 handlers

🤖 Agent 运行时 · Pi SDK 0.56.3

来源

  • 作者:Mario Zechner
  • npm scope@mariozechner
  • GitHub repobadlogic/pi-mono(monorepo · 同一个人,不同 handle)
  • License:Apache 2.0

Pi SDK 四件套(monorepo 同 version)

用途Lynn 是否用
@mariozechner/pi-coding-agentAgent 主循环 + tools(read / write / edit / bash / grep / find / ls)
@mariozechner/pi-aiProvider 抽象(OpenAI-compat)
@mariozechner/pi-agent-coreAgent 核心抽象
@mariozechner/pi-tuiTerminal UI❌ 传递依赖,未使用

Lynn 在 Pi SDK 上打的 9 个 patch

脚本路径:scripts/patch-pi-sdk.cjs(postinstall 自动执行)

#Patch目的
1sdk.js import streamSimpleheader 透传入口
2createAgentSession 的 baseToolsOverride自定义工具能替换内置默认
3sdk.js request headers/metadata 流入 providers跨层 header 贯通
4openai-completions.js strip empty tools arraydashscope / volcengine 兼容(不接受空 tools)
5openai-completions.js zai-thinking-format智谱 GLM reasoning 格式修正
6openai-completions.js Brain tolerant adapter helper兼容 Brain 的自定义 SSE
7openai-completions.js route Brain through tolerant adapter路由接入
8compaction.js tolerate missing usage空 usage 不崩
9agent-session.js tolerate missing usage in stats统计兼容

💾 数据存储

组件版本 / 参数用途
better-sqlite312.6.2(WAL 模式)SQLite 底层
FTS5 虚拟表tokenize unicode61全文检索
向量检索vector-interface.js(Lynn 自研)语义检索
数据落盘~/.lynn/agents//memory/每 Agent 独立 store

🧠 记忆系统(Lynn 自研)

路径lib/memory/ · 15 个文件 · 5038 行代码

对 Pi SDK 扩展,不是 Pi SDK 自带功能

六层架构

#关键文件行数
1Fact Store · 事实存储fact-store.js765
2Deep Memory · 深层记忆deep-memory.js + compile.js652
3Proactive Recall · 主动召回proactive-recall.js287
4User Profile · 用户画像user-profile.js + inferred-profile.js570
5Project Memory · 项目记忆project-memory.js398
6Skill Distiller · 技能蒸馏skill-distiller.js599

辅助模块

文件行数用途
memory-ticker.js568每 6 轮滚动摘要
vector-interface.js381向量检索引擎
session-summary.js356会话摘要
retriever.js171三路融合检索(标签 + FTS5 + 向量)
memory-search.js171检索 v2 API
config-loader.js139配置加载
session-stats.js101会话统计

🌍 Bridge 适配器(Lynn 自研)

路径lib/bridge/

平台适配器
微信
飞书
QQ
企业微信
Telegram
Discord

🛡 安全沙箱(Lynn + pi-coding-agent 协作)

组件实现
路径 ACLPathGuardLynn 写的(四级:BLOCKED / READ_ONLY / READ_WRITE / FULL)
沙箱(macOS)sandbox-exec + 动态 Seatbelt SBPLOS 级
沙箱(Linux)Bubblewrap (bwrap)OS 级
注入检测ClawAegisLynn 写的(纯正则,<10ms)
行为模式SAFE / PLAN / FULL用户可选

☁️ 云端后端(独立服务,不是 desktop 一部分)

Brain(Tencent Cloud · Node.js)

  • 路径/opt/brain/ on 腾讯云
  • 协议:OpenAI-compat
  • 核心机制:Plan C 工具透传(让默认模型也能调 write / edit / read / bash

六级降级链

Tier模型上下文类型
T1GPU Qwen3.6-35B-A3B AWQ-4bit128K自建 vLLM 推理
T2Moonshot Kimi K2.5256K云 API
T3智谱 GLM-4.7128K云 API
T4智谱 GLM-5.0-Turbo128K云 API
T5DeepSeek V3.2128K云 API
T6Step-3.5-Flash / MiniMax M2.5128K云 API 兜底

GPU 推理服务器

  • 连接方式:SSH tunnel 端口 18000
  • 推理引擎:vLLM
  • 量化:compressed-tensors + Marlin kernel + FP8 KV cache
  • KV cache 容量:12.39 GiB = 192K tokens
  • 最大并发max-num-seqs 32

📊 代码规模统计

模块文件数行数
记忆系统 lib/memory/155038
工具 lib/tools/24
安全沙箱 lib/sandbox/
Bridge lib/bridge/6
Plugin 系统 core/plugin-manager.js1413
Skill 数据集 skills2set/33 个内置 Skills
Brain 服务器 server.js1~8000+

🚀 构建 / 发布

工具用途
Vite 7前端构建(renderer + main)
@vercel/nftNode 生产依赖追踪
electron-builder 26打包 DMG / EXE
@electron/notarizemacOS 公证

交付形态

平台文件签名状态
macOS Apple SiliconLynn-0.76.2-macOS-Apple-Silicon.dmg✅ Developer ID 签名 + 公证
macOS Intel同 DMG
Windows x64Lynn-0.76.2-Windows-Setup.exe⚠️ 未签名(首启 SmartScreen 需放行)
Linux计划中
好,开始讲坑。

坑 #1:Pi SDK 0.67.68 升级后,主链路 39ms 立挂

我用 @mariozechner/pi-coding-agent 作为 agent 运行时(类似 Claude Code 底层用的 harness),之前锁定在 0.56.3。

发版前想顺手升级到最新的 0.67.68,理由很简单 —— 带了一堆 bug 修复和性能优化。

结果一打开 Lynn,输入"今天金价",39 毫秒后 assistant reply done:

[13:30:21.245] [INFO] [ws] user message (6 chars, 0 images)
[13:30:21.284] [INFO] [ws] assistant reply done    ← 39ms!

正常 Brain 调用要几秒,39ms 意味着 Pi SDK 直接失败了。

深挖发现:Pi SDK 0.67.68 的 openai-completions.js 引入了一个 "Brain tolerant adapter",强制把 params.stream = false:

function streamBrainTolerantOpenAICompletions(model, context, options) {
    // ...
    const params = buildParams(model, context, options);
    params.stream = false;   // ← 致命一行
    // ...
}

这个 adapter 历史原因是老 Brain 的 SSE 不太规范,非流式绕过 OpenAI SDK 的严格解析器。

但我的 Brain 是标准 SSE,被这个过度防御坑了 —— 非流式意味着 answer 要完整生成才返回,叠加客户端工具透传机制没接,最终 Pi SDK 直接抛错,主链路 39ms 立挂。

最终决定:回滚到 0.56.3

升级 Pi SDK 的红利(Qwen chat-template 修复、prompt_cache_key 对齐等)对我的场景价值不大,而回滚是 1 条命令的事。

教训:不要在发版前做"顺手升级核心依赖"的操作。


坑 #2:Brain tolerant adapter 的历史包袱,一刀撤掉反而对了

回滚 Pi SDK 到 0.56.3 之后,我又发现 0.56.3 里也有这个 adapter(只是代码略不同)。

仔细想了想:这个 adapter 的副作用是所有 brain 请求不流式,用户体感是"发完消息,15 秒没动静,然后一下子蹦出来 2000 字"。

产品上这叫灾难。Claude / ChatGPT 的流式打字体验,是把"等待 15 秒的焦虑"转化成"看字在吐的爽感"。

我做了一个带风险的决定:修改 patch-pi-sdk.cjs,直接把 brain branch 删掉,让所有 brain 请求走正常 SSE 流式路径。

// patch-pi-sdk.cjs - 把原来注入的 brain branch 删除
// 原代码:
//   if (model.provider === "brain" || baseUrl.includes("api.merkyorlynn.com")) {
//     return streamBrainTolerantOpenAICompletions(...)
//   }
// 删除后:brain 走 normal stream,同其他 provider

重新打包,验证:

  • ✅ 答案一段段流式出现(不再"一下子蹦出来")
  • ✅ 工具进度气泡实时可见(之前是最终答案里一起塞)
  • ✅ 中文 thinking 流式展开 ...

等等,"中文 thinking 流式展开"是什么?这里就涉及到下一个坑了。


坑 #3:Qwen3.6-35B-A3B 执意用英文思考,prompt 都改不动

Lynn 的默认模型是 Qwen3.6-35B-A3B(3B active MoE),本地 4090 部署,vllm serve。

问题:中文 query,它的思考过程 91% 是英文

用户: 用一句话告诉我什么是梅森素数

模型 thinking:
Here's a thinking process:
1. **Analyze User Input:**
   - Question: 用一句话告诉我什么是梅森素数
   - Key Requirements: Define Mersenne prime
...

对中文用户来说,展开思考块看到整屏英文,体验很糟。

我尝试了 3 种方案:

  1. System prompt 强硬要求中文思考 — 13.4% 中文,无效,模型固化偏置
  2. vllm chat_template_kwargs: { enable_thinking: true } — 不生效,Qwen3.6 训练数据里没有 <think> 标签习惯
  3. vllm continue_final_message: true + assistant 前缀 prefill91.4% 中文,成功!

最后方案的代码:

// brain 端 callGpuQwenCoder 内
const __cjkChars = (lastUserText.match(/[\u4e00-\u9fff]/g) || []).length;
const __isCnQuery = __cjkChars > 0 && __cjkChars / lastUserText.length > 0.3;

if (__isCnQuery && round === 0) {
  msgs.push({ role: 'assistant', content: '好的,让我用中文想一下:' });
  body.continue_final_message = true;
  body.add_generation_prompt = false;
}

模型收到 assistant 前缀后会"接续"写,天然继承中文语气。这招对任何训练偏置都有效


坑 #4:Intel Mac 死机 - 藏了一个版本的 BUG

打包 macOS dmg 时,我看到一行 warning:

• file source doesn't exist  from=/Users/lynn/Downloads/Lynn/dist-server/mac-x64

看起来是 warning,electron-builder 正常继续,dmg 也打出来了。我差点跳过了。

但仔细一想:Intel Mac 用户装这个 dmg 会发生什么?

Lynn 的 server bundle 带 native deps(better-sqlite3koffi 等),这些 .node 文件是架构特定的。

我的 fix-modules.cjsdist-server/${platform}-${arch}/ 复制 native deps 到 app 里:

const serverBuildModules = path.join(
  __dirname, '..', 'dist-server',
  `${osDirName}-${arch}`, 'node_modules'
);

if (fs.existsSync(serverDir) && fs.existsSync(serverBuildModules)) {
    fs.cpSync(serverBuildModules, serverNodeModules, { recursive: true });
}

dist-server/mac-x64 不存在(我只跑过 node scripts/build-server.mjs 默认 darwin arm64)!

验证:

# Intel dmg 里的 better-sqlite3 native binary
$ file dist/mac/Lynn.app/Contents/Resources/server/node_modules/better-sqlite3/build/Release/better_sqlite3.node
(file not found!)

# ARM 版本就有
$ file dist/mac-arm64/Lynn.app/Contents/Resources/server/node_modules/better-sqlite3/build/Release/better_sqlite3.node
Mach-O 64-bit bundle arm64

这个 bug 在 v0.76.1 就有! 只是没人反馈 —— 要么 Intel Mac 用户没多少,要么他们装完启动失败直接删掉了应用。

修复:打 Intel dmg 前先生成 x64 server bundle:

node scripts/build-server.mjs darwin x64

然后 Intel dmg 从 112MB → 182MB(补上了 70MB 的 x64 native deps,better-sqlite3 等全是 Mach-O 64-bit bundle x86_64)。

教训:发版前 warning 不是装饰,是 error 的前兆。


坑 #5:模型调 read_file,Lynn 工具叫 read,死循环开始

发版后用户反馈:"你让 Lynn 读个小说文件,它卡死了"。

看 brain 日志:

GPU round 0: server=[] client=[read_file]
GPU emit client tool_calls to Lynn: read_file

模型调的工具名是 read_file(下划线风格,OpenAI 生态常见)。但 Lynn 的实际工具叫 read(没下划线,Pi SDK 命名约定)。

Lynn 收到 read_file → ToolRegistry 找不到 → silently fail → 模型看到 tool 执行失败 → 思考"让我再读一次" → 再调 read_file → 再失败 → 死循环

修复一行代码搞定:

// brain 端 emit tool_calls 前做 name normalization
const __nameAliases = {
  read_file: 'read',
  write_file: 'write',
  edit_file: 'edit',
  list_files: 'ls',
  list_dir: 'ls',
  search_files: 'glob',
  view_file: 'read',
};

for (const tc of toolCalls) {
  const orig = tc.function.name;
  if (__nameAliases[orig]) {
    tc.function.name = __nameAliases[orig];
    log('info', 'ai', `tool alias: ${orig} -> ${tc.function.name}`);
  }
}

教训:LLM 的工具调用名很容易"想象"成 OpenAI 生态里常见的名字(哪怕你的 tool schema 已经声明不是这个名)。给一个 alias map 是一分钟的事,能救命。


坑 #6:repetition_penalty 加了治死循环,反而堵塞正常输出

死循环修好之前,我试着加 repetition_penalty: 1.08 + frequency_penalty: 0.3 来强抑制重复 thinking。

单元测试通过 ✓,brain smoke 通过 ✓,直接上线。

结果用户发现:

用户: 读 /path/to/file.md
Lynn: "文件似乎"
      (然后没了)

3 个字就结束!再试:

用户: 另一个 query
Lynn: "好的," (然后没了)

看 brain 日志:

GPU round 0: thinking=0ch + answer=0ch   ← 模型完全吐不出字
Qwen3.6-35B-A3B stream empty, trying non-stream fallback
GPU round 0: thinking=0ch + answer=3ch

原因:在长 context(memory summary + 历史对话 + tool_result)后,frequency_penalty=0.3 是累计出现次数惩罚,常见 token(的/是/了/一)已经出现 N 次,每个候选 token 都被强罚,模型最终选不出字,直接 EOS。

model.forward(context) → next_token_logits
    ↓ repetition_penalty (最近重复的 token)
    ↓ frequency_penalty (累计出现次数)
    ↓ 所有合理候选都被罚 → top-p 采样选到 EOS

解决方案:全部撤回。死循环的真正修复是坑 #5 的 name alias(修好工具调用就不循环了),penalty 是多余的,且有副作用。

教训:LLM 调参时 "副作用 > 收益" 是常态。加参数前问自己:"能不能不加?有没有更底层的 bug 应该先修?"


坑 #7:vllm max-model-len 128K vs 64K 的并发陷阱

Lynn 默认模型最初配置 max-model-len 131072(128K 上下文),看起来"上下文越大越好"。

但,我一算:

GPU:                RTX 4090 48GB
gpu-mem 0.80       19.2GB  vLLM
Qwen3.6-A3B FP8:    ~17GB (实际是 AWQ 4bit ~9GB,但我一开始以为是 FP8)
KV cache pool:      2.2GB (FP8 估算)  13GB (AWQ 4bit 实算)

关键:vLLM scheduler 按 max-model-len 预留 buffer 分配 batch slot,128K 意味着 worst case 单 request 占用巨大,自然压低并发。

实际测算用户 query 分布:

  • 短问答(<8K):95%
  • 单章小说(10-20K):4%
  • 整本书 / 多章节(>64K):< 1%

那 < 1% 走 fallback 到 T2 的 GLM-4.7(128K)或 T3 Kimi(200K)就够。

修改配置:

--max-model-len 65536           # 128K → 64K
--gpu-memory-utilization 0.92   # 0.80 → 0.92
--max-num-seqs 32                # 保持

效果:

  • KV pool 2.2GB → ~13GB(2.3× + AWQ 4bit 权重小)
  • 实际并发 ~9 → ~50(5× 提升)
  • 单 request 最大上下文仍然够 99% 场景

教训:"上下文大 = 好" 是新手陷阱。真实用户 query 分布决定了最佳配置。


5 小时过去了,我看着 dist/ 目录

Lynn-0.76.2-macOS-Apple-Silicon.dmg   173 MB  ✓ 公证完成
Lynn-0.76.2-macOS-Intel.dmg           182 MB  ✓ 公证完成(终于带上 x64 server!)
Lynn-0.76.2-Windows-Setup.exe         195 MB  ✓ 签名完成

截屏2026-04-19 01.08.17.png

发到 GitHub Release,同步到国内镜像站,更新 download.html 版本号。

发版完成。

反思:一下午踩了 7 个坑,到底说明什么

  1. 软件的历史包袱是慢性毒药。Brain tolerant adapter 当年是合理防御,今天变成最大 UX 杀手。每半年应该主动审视"这个代码当初为什么存在?今天还需要吗?"

  2. 依赖升级在发版前是高风险动作。Pi SDK 0.56.3 → 0.67.68 的"顺手升级"差点毁了整个发版。独立开发者资源有限,能用稳定版别追新。

  3. Warning 不是装饰file source doesn't exist 这种被忽视的 warning,背后是 Intel Mac 用户的死机 bug。

  4. LLM 调参别当银弹repetition_penalty 看起来能治死循环,实际是 tool_name 幻觉没修,模型压根没法跑通工具调用。底层 bug 修好,大量"疑难杂症"自己消失。

  5. 真实用户 query 分布驱动配置。"上下文越大越好"是直觉陷阱,99% 的用户永远用不上 128K,但 5× 并发是天壤之别。

  6. 承认自己记错了。我一开始以为 GPU 上是 FP8 模型(前一天下的),KV pool 算法全错。后来发现是 AWQ 4bit,整个吞吐预估大幅修正。不要懒得去 cat /etc/systemd/system/vllm-qwen35.service

  7. Dogfood 不可替代。这 7 个坑里的 5 个,是我自己用 Lynn 时发现的。自测比单测 test case 残酷 100 倍。


Lynn 在哪

Lynn 是 Apache 2.0 开源的私人 AI 桌面助手,带长期记忆、任务模式切换、多 Agent 协作。这一版(v0.76.2)的主要改进:

  • ✨ 工具进度气泡(就是坑 #2 打通的链路)
  • ✨ 中文思考链(坑 #3 的胜利)
  • ✨ Intel Mac 真的能用了(坑 #4 的修复)
  • ✨ 工具名 alias 防幻觉(坑 #5)
  • ✨ 4090 并发 5× 提升(坑 #7)

如果你也在做 Electron + LLM + 本地 GPU 的折腾,或者只是好奇一个"有灵魂"的 AI 长什么样:

评论区拍砖,有任何"我也遇到过 X"的经历欢迎交流。


(完)