Skip to content

关于 GenericAgent 的一些修复和问题 #813

Description

@GiftedScout

写给上游维护者。我是 GiftedScout,fork 在 https://github.com/GiftedScout/GenericAgent

这段时间我主要在 GA 上跑长任务,遇到问题就顺着代码往下查,能复现根因的我直接在自己的 fork 上修了,修不了的整理成问题。这份文档分两部分:第一部分是已经改好、可以提 PR 的修复,第二部分是我认为需要讨论的问题。所有 commit 都在我 fork 的 main 分支上,测试全部实际跑过,不是编译通过就算数。

我一直很赞成GA的理念:主提示词很克制,没有堆砌,模型能用的上下文余量比很多同类工具大;另外 llmcore、agent_loop、TUI 都是可读的纯 Python,问题能一路追到底层 SSE 解析,这是我能快速定位这些根因的前提。下面的问题我理解多半是迭代中各自为战、缺一条统一契约造成的,不是谁写得差。

对照说明:上游 origin/main 在我分叉点(f6e5657)之后只加了 6 个提交(abort 唤醒、TTFT、hub 之类),和下面的改动没有冲突。所有修复目前都只在我 fork 里,上游一行没合。

第一部分里我把修复分了 A、B 两类:

  • A 类:修的是 origin 原生代码就有的 bug。cherry-pick 到我 fork 的对应 commit 就能合,不依赖任何 fork 私有代码。
  • B 类:修的是我自己在 fork 上加的机制里的问题,或者现象在 origin 存在但解法是我从零搭的。

第一部分:已经修好的(可提 PR)

A 类:origin 原生 bug,可以直接合并

1. 多模态图片输入被静默丢弃

这个我觉得是最严重的一个。给 多模态模型贴图片,模型根本看不见图,只能靠 ocr 工具兜底,结果时好时坏。

根因是 origin 代码里三处串联:

  1. agentmain.py 的 run() 里 task["images"] 取出来之后从来没有被用过,发给模型的只有 TUI 展开出来的路径文本,图像字节根本没进请求;
  2. llmcore.py 的 _to_responses_input()(origin 大约 579 行)只认 image_url 块,Claude 风格的 image 块被直接丢掉,所以走 responses 端点的多模态模型永远收不到图;
  3. NativeToolClient.chat() 里过滤空白文本块的逻辑,会把没有 text 键的图片块一起过滤掉。

修复就是三处对应改:run() 把图片按 Claude 风格块并进首条用户消息;_to_responses_input 加 image 块到 input_image 的转换;过滤器改成保留所有非 text 块。commit 是 28f71c4。

验证:tests/test_fixes_multimodal_cache.py 22 个断言,另外我做了端到端验证——图片字节穿透整条链路送到线上模型,让模型读出只存在于图里、prompt 里根本没有的字符串,读出来了。

局限:音频视频这类非图片多模态还是没打通,这个归到第二部分的问题 11 了。

2. responses 模式不回显思考

DeepSeek 这类走 responses 端点的模型,思考完全不显示;同一个模型换 chat_completions 端点就正常。根因是 llmcore.py 里 responses 分支对 reasoning_text.delta 只做累积,从来不对显示流 yield;chat_completions 分支是有完整信封的,两个分支行为不一致。修复就是让 responses 分支对齐:流式思考始终上屏(前端折叠窗口防刷屏),是否入库仍然由 omit_thinking 决定。也在 28f71c4 里,回归测试有字节级一致断言。

3. 激进压缩打断前缀缓存,反而更贵

DeepSeek 这种缓存命中价很低、未命中很贵的模型,实际成本一直比预期高。查下来是 trim_messages_history() 每 7 轮就对旧消息做原地截断,前缀每轮都在变,前缀缓存持续失效,等于每轮都按全价 input 付钱。修复:没超过 cap 就绝不改写历史(前缀逐字节稳定就能命中缓存),真正超限才压缩加裁剪。也是 28f71c4,回归断言"未超限历史字节不变、超限能收缩"。

4. responses 分支漏传 temperature

chat 分支传 temperature,responses 分支漏了。官方 Responses API 是支持这个字段的(默认 1.0),所以用户在配置里写的 temperature 对所有 responses 端点是静默失效的。665604a 修的。验证:tests/test_responses_payload.py 用 monkeypatch 捕获真实 payload,5 个断言。

5. reasoning_effort 白名单拒绝 ultra

_enum 的白名单只有 none/minimal/low/medium/high/xhigh/max,GPT-5.6 新加的 ultra 档位被静默丢掉,只打一行 WARN。665604a 一起修的,回归断言 reasoning.effort 原样透传 ultra。

6. TUI 长 URL 被折行打断,Ctrl+点击失效

正文里的长 http 链接会被 CJK 换行补丁从中间折断(比如 ...docs/s 换行 ome/...),终端就不认它是链接了,Ctrl+点击打不开。根因是 frontends/tuiapp_v2.py 里替换 Rich 原生换行的 CJK 补丁(_cjk_divide_line / _cjk_compute_wrap_offsets)对没有 CJK 的长词(就是 URL)逐字符折行。修复:加 _url_fold(),URL 只在 / 边界折行,接进两条换行路径。665604a。我用 60 列无头渲染复现过:修复前 URL 从中间断,修复后在 / 边界完整折行。

B 类:fork 侧机制,作为特性提案或者留 fork 自用

7. 长轮次后记忆吞掉对用户的回答

现象:长任务跑完,用户最后看到的不是回答,而是 L1/L2 记忆内容。推理型模型(Qwen)是中间轮次已经回答了,结算工具一触发,记忆输出把它盖掉了;非推理型模型(GPT)干脆连回答都没了。

这个现象在 origin 上就会发生:origin ga.py 527 行的 do_start_long_term_update 把结算提示词作为一条新的 user 消息注入,模型会当成面向用户的请求来答;而且结算轮的文本走的是普通显示通道,物理上就覆盖了主回答。

但要说明清楚:origin 没有任何结算显示隔离机制(我查过,origin 的 agentmain.py 和 agent_loop.py 里 settlement 相关代码是零处)。下面这套方案是 fork 从零搭的,不是对 origin 某段代码的修改:

  1. agent_loop 在答案轮末 yield 一个 settlement 标记,agentmain 的显示累积抽成 iter_display_events(),收到标记就冻结显示通道——记忆维护文本只进 history(审计还在),不进 TUI 显示;
  2. TUI 的 spinner 在结算期间切成琥珀色的"记忆结算中…(耗时、第几轮)"状态,结束自动恢复,/continue 重放也不会再注入答案文本;
  3. 中间还修了我自己 fork 的一个隐蔽回归(32a2ed2):结算分支引用了一个不存在的常量 C_YELLOW,NameError 被 spinner 的 try/except 吞了,表现就是旧的文字冻结在那儿不刷新,未保护的路径还会把 Textual 直接炸崩。这条只跟 fork 有关。

相关 commit:28f71c4(提示词部分)、665604a(通道冻结)、cddcc5d 和 e8388f8(结算 UI 状态)、32a2ed2(NameError)。验证:tests/test_settlement_display.py 25 个断言,驱动的是真实的 iter_display_events 代码路径;tests/test_settling_spinner_render.py 防 spinner 回归。

如果认同"结算不该覆盖用户答案"这个方向,这套机制可以整体作为特性 PR。最小可移植的版本是一行提示词修改(28f71c4 里那部分,直接就能用)加显示通道冻结(大约 130 行)。

8. 思考信封对正文里的字面思考标签串敏感

先交代背景:思考显示信封(_visible_content / _disp_think_escape 这套)是我 fork 加的,origin 的 llmcore 里这些符号一处都没有。所以这条 bug 和它的修复都发生在 fork 机制内部,没法直接 cherry-pick 到 origin。

fork 的思考信封出过两起事故:一次是 CoT 尾段加工具调用刷屏,一次是答案尾部被吞成思考。查下来比"正文没做转义"更深一层:SSE 分帧会把标签字面量拆成两半,逐帧转义对半个标签是失效的,两半在显示流里重组之后,TUI 的配对正则提前闭合了思考信封。我在真实流里实测过,9 处标签 8 处被帧边界拆开。

修复是 6fb6034 的锚定式拆分器(标签只在流首或换行后翻转状态,行中的、反引号包着的都当字面量放行),加 f6eaf8e 的跨帧回卷缓冲(尾部可能是标签前缀的部分留到下一帧拼齐再整体转义,流末 flush 原样输出,防止把 a < b 这种误伤)。验证:tests/test_think_anchor.py 24 个断言全过,真实流回放里 CoT 泄漏从 4 处清零,答案完整。

残余问题:CoT 里"独占一行的裸闭标签"还是会被锚定规则当成边界。真实模型引用标签都会用反引号,只有恶意 prompt 能触发;彻底解法是流末重算,但那样答案要延迟到流尾,UX 不划算,我没做。

这部分的价值在于:如果上游将来要给 origin 加思考流式显示,这套锚定加跨帧缓冲的设计可以直接采用,能少走我踩的弯路。

9. SSH 隧道自愈是死代码

这条也是 fork 基础设施:SSH 隧道是我 fork 加的。但值得提,因为 origin 的 _stream_with_retry 把一切异常吞掉、转成 yield 的错误字符串这个模式是两边同源的,origin 将来加任何重试场景都会踩同一个坑。

具体说:raw_ask 里写了"Connection refused 时重建隧道"的自愈分支,但因为异常到不了 raw_ask,这段是死代码;而且触发条件只匹配 refused,漏掉了 reset/aborted/RemoteDisconnected/Broken pipe 这些流中断。修复是把隧道重建下沉到重试路径,命中隧道死亡签名集且配置了 ssh_tunnel 时,重试前先重建,复用现有重试预算。aedce34。验证:tests/test_tunnel_selfheal.py 13 个断言,monkeypatch 注入故障,不依赖真实 ssh 和网络。

10. fork 侧功能增强(不请求合并,说一下现状)

  • /continue 手动重放会话(f129cbe),对应第二部分问题 10 的半解决;
  • /addkey 免开 mykey.py 直接加端点,加 native_config 单字典布局和迁移向导(9f8cb12、e717ec9、10dd469、3d5d97f,mykey_admin.py 是 fork 新文件),对应问题 12 的半解决;
  • 粘贴图片直传加 ocr 工具纯本地化(ca8fbc7,media_api.py 是 fork 新文件)。

测试全集:test_fixes_multimodal_cache.py(22)、test_tunnel_selfheal.py(13)、test_responses_payload.py(5)、test_settlement_display.py(25)、test_settling_spinner_render.py(2)、test_think_anchor.py(24)、test_media_tools.py(11)、test_addkey_tui.py、test_mykey_provider_dict.py,都在 fork 的 tests/ 下,全部实际跑过。A 类 6 项的测试可以随 PR 原样提交。


第二部分:想讨论的问题

1. responses 分支硬编码了 Codex CLI 专属字段

origin llmcore.py 526-527 行,responses 分支写死了 prompt_cache_key、client_metadata(x-codex-window-id / x-codex-installation-id)、include 里的 reasoning.encrypted_content。这明显是按 Codex CLI 抄的,对通用的 OpenAI Responses 兼容端点并不是必需的。

我暂时保留了:日志里的 400 全是 context size 超限,没有字段被拒的证据,而且 prompt_cache_key 对缓存命中是有帮助的。建议按端点门控——api_base 含 openai.com 或者显式配 codex_compat: true 才带这些字段,其他端点不带。这个需要你来定。

2. 输出撞到 max_tokens 没有自动续写

finish_reason 是 length 的时候,现在的行为是追加一个截断标记然后本轮结束,没有"接着上文继续"。fork 里我加了 /continue 可以手动重放(f129cbe),重放视图和实时显示一致,但自动续写还是没有。

这里有个容易忽略的点:思考也吃这个预算(官方文档确认 max_output_tokens 包含推理 token)。思考型模型(比如 qwen 系开 xhigh)长轮次里光是思考就能吃满,正文还没开始就被掐断了。更麻烦的是截断如果落在工具调用参数中间,json.loads 直接炸,整轮报错。

建议:截断时自动发一个"从断点继续"的续写请求;至少也要对 length 截断的 tool-call 参数做完整性检查再入队。

3. 多模态只有图片通道

音频、视频输入没有对应的协议路径(chat 的 input_audio、responses 的 input_file 都没实现),非图片多模态只能靠本地工具兜底。属于设计缺口。建议按端点能力显式探测(/v1/models 的 capabilities,或者配置里显式声明)再决定路由,不要默认全都有或者都没有。

4. 配置全靠手改 mykey.py

key、上下文窗口、模型名(大小写和连字符都是敏感的)都靠手改文件。_enum 遇到非法值只打一行 WARN 就忽略了,错误是静默的

fork 里我已经做了 /addkey 和 native_config 向导,不用开文件就能加端点。还缺的是 /models 或者类似的配置检查命令:列出已配置的端点、连通性探测结果、非法字段清单。

5. Wayland 下桌面操控能力不完整

我本机 XDG_SESSION_TYPE=wayland,键鼠控制依赖 win32/X11 路径,在 Wayland 下能力是残缺的。建议加 wlr / hyprctl / ydotool 或者 AT-SPI2 后端;如果短期不做,至少探测到 Wayland 环境时明确提示一句,不要静默失败。

6. 浏览器桥接的登录态不保留

TMWebDriver(ws:18765)没有 token,登录态依赖真实浏览器 profile,很多站点操作几下就掉登录。建议桥接侧支持持久化 profile 目录加 cookie 存储,或者让用户复用日常浏览器的 profile 并显式声明风险。

7. 产物全堆在 temp/,难找也预览不了

模型经常把最终汇报和结果写进 temp/,事后不好检索,也预览不了。建议约定一个 deliverables/ 输出目录,加个索引或者 /deliverables 命令,长报告落盘成 md 并给出可预览的路径。

最后希望GA能在设计上加快进步,codex类闭源生态需要自己适应而不能自定义,dsh类破坏性更新太多还不稳定,也是我坚持在GA自改进的原因

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions