AI Agent实现推理过程
从“黑箱 Agent”到“可观察 CLI”:我是怎么补上推理摘要,并彻底拦住错误 HTML 的
前段时间,我在自己的 RG CLI 项目里连续踩中了两个很影响体验的问题。
第一个问题是:模型其实已经在“想”,但终端里完全看不到任何推理摘要。用户只能看到工具调用和最终答案,整个过程像一个黑箱。
第二个问题是:在接入 OpenAI-compatible 的 responses 能力后,如果 baseUrl 配错了,或者上游网关返回了错误页,程序拿到的可能是一整段 HTML,而不是我真
正需要的 JSON/SSE 响应。对用户来说,这种体验非常差;对开发者来说,排查成本也很高。
这次改造,我主要做了两件事:一是把“推理摘要”真正接进来,并在 CLI 里以合适的方式展示;二是把所有“错误 HTML”在进入业务逻辑前拦住,让系统只处理它该处
理的数据。
| 项目 | 改造前 | 改造后 |
|---|---|---|
| 推理过程 | 终端看不到,只有最终答案 | 支持实时 Thinking 和最终“思考摘要” |
| 工具调用顺序 | 思考信息和工具信息容易脱节 | 思考摘要按轮次插入,和工具调用时间顺序一致 |
| HTML 错页 | 容易变成模糊报错,甚至污染响应链路 | 在 JSON/SSE 两条链路统一拦截,明确提示 baseUrl 或网关问题 |
| 排障体验 | 只能猜哪里错了 | Debug 下直接看到请求体、状态码、content-type 和响应片段 |
一、推理摘要:不是“显示更多文本”,而是把能力链路补完整
改造前的问题
在最早的实现里,查询主循环只关心三件事:发消息、拿工具调用、拿最终文本。也就是说,模型返回的“推理摘要”根本没有正式的数据通道。
这会带来两个直接问题:
- 用户只能看到结果,看不到思路,Agent 的行为不够可解释。
- 即便底层模型已经支持 reasoning summary,CLI 这一层也接不住,功能等于白开。
更关键的是,推理信息不是简单 console.log 一下就行。它至少要经过四层:
- 配置层要能打开它。
- LLM 请求层要真正向上游声明需要 reasoning summary。
- 流式解析层要能识别 summary 相关事件。
- 会话/UI 层要知道哪些该实时显示,哪些该持久化保存。
少一层,这个功能都不算真正完成。
我是怎么改的
这次改造里,我先把配置打通了,新增了 reasoningEffort 和 reasoningSummary,并让它们能从默认值、用户配置、环境变量、命令行参数一路透传到模型请求层。
然后在 OpenAI-compatible 的 responses 请求里,显式带上 reasoning 配置,并补上 include 与 text 控制。这样不是“等模型碰运气返回”,而是主动告诉上游:
我要 reasoning summary,而且我要按可解析的结构给我。
接着我单独补了一条 responses 的流式解析链路,专门处理这类 SSE 事件:
- response.reasoning_summary_part.added
- response.reasoning_summary_text.delta
这一层做的事情很关键:不是只收文本,而是把分段 summary 重新拼起来,并且在需要时发出 section break,这样 UI 才知道什么时候该换段、什么时候该继续追
加。
最后,QueryEngine 和 UI 也一起改了:
- 运行中用 ThinkingPanel 展示实时思考文本
- 结束后把它整理成“思考摘要”
- 只持久化摘要,不持久化零散的实时推理片段
这里我刻意做了一个取舍:保留“可观察性”,但不把完整思考流原样落盘。这比一股脑全存下来更稳,也更符合 CLI 场景。
和改造前的对比
改造前,用户看到的是:
“提问 -> 工具调用 -> 最终答案”
改造后,用户看到的是:
“提问 -> 实时 Thinking -> 工具调用 -> 对应轮次的思考摘要 -> 最终答案”
而且我后面又补了一次修正,把“思考摘要”和“工具调用”的展示顺序按时间线重新排了一遍。否则即使有摘要,也会出现“所有思考都堆到最后”的问题,信息是有了,
但时序是错的,读起来还是别扭。
这也是为什么我后来又提交了一次顺序修复:功能实现只是第一步,展示顺序正确才算真正可用。
二、错误 HTML:本质不是返回错了,而是没有在边界处拦住脏数据
改造前的问题
这个问题表面上看是“接口返回了 HTML”,本质上其实是:程序没有把“模型响应”和“网站错误页”当成两种完全不同的数据处理。
尤其是在接入 responses 流式能力之后,链路比原来更复杂了:
- 普通 JSON 请求是一条入口
- 流式 SSE 请求又是一条入口
如果只在普通请求里做保护,而流式入口没有校验,那么一旦 baseUrl 指向了网站首页、反向代理页、网关错误页,程序就可能拿到一段 HTML。接下来要么解析失
败,要么给出很模糊的异常信息,最糟糕时还可能让错误页混进正常响应流程。
我是怎么改的
这次我做的不是“补一个 if”,而是统一了边界校验策略。
先在请求层抽出 HTML 检测逻辑,只要 payload 是字符串,并且以 或 <html 开头,就直接判定这不是模型响应。
然后分两条链路处理:
- 对普通 JSON 请求,在拿到响应文本后先安全解析;如果发现是 HTML,不进入业务解析,而是直接构造明确错误信息。
- 对流式 SSE 请求,不仅检查状态码,还额外检查 content-type 是否真的是 text/event-stream。哪怕 HTTP 200,只要内容类型不对,也不继续往下读。
这一步很重要。很多“错误 HTML”并不是 500,它可能是一个“成功返回的网页”,如果你只看状态码,根本拦不住。
另外我还把 debug 信息补全了。现在一旦遇到 HTML,错误里能直接看到这些关键信息:
- 请求 URL
- 请求体
- HTTP 状态码
- content-type
- 响应片段
这样排查时基本不用再猜,看到信息就知道是:
- baseUrl 指错了
- 路径拼错了
- 还是上游网关直接吐了个错误页
和改造前的对比
改造前,遇到 HTML 更像是“程序突然坏了”,你只能顺着异常一点点猜。
改造后,系统会明确告诉你:
“这里返回的不是 JSON/SSE,而是 HTML;你的 llm.baseUrl 很可能不是 API 根地址,或者上游返回了网关错误页。”
这两种体验差别非常大。前者是被动排错,后者是带诊断信息的失败。
三、这次改造里,我最看重的其实不是功能,而是边界
如果只从提交记录看,这次最显眼的两个点是:
- ee9f25d:实现思考过程摘要功能
- 2fbee16:修正思考过程和工具调用的时间顺序
但如果从工程角度看,我真正想解决的是两个更底层的问题:
- 模型的新能力,不能只停留在 API 层,要一直接到用户真正看得见的地方。
- 任何非预期响应,都必须在边界处被识别和隔离,不能流进核心业务。
推理摘要的本质,是把 Agent 从“黑箱”变成“可观察系统”;错误 HTML 的修复,本质,是把系统从“脆弱链路”变成“有防线的链路”。
这两个问题看起来一个偏体验,一个偏稳定性,但最后都指向同一件事:一个能用的 CLI Agent,不只是能答题,更要让人知道它在做什么,以及它为什么会失败。