Commit Graph

5 Commits

Author SHA1 Message Date
f7dad243b6 fix(deepseek): reasoning_content 三层贯通 + thinking 关闭层级修正
## 生产故障

    all 2 providers failed, last error: api error 400:
    "The reasoning_content in the thinking mode must be passed back to the API."

## 两个根因,第一个更根本

### ① thinking 关闭一直没生效(真正的触发器)

deepseek.lua 写的是 `req.extra_body.thinking = {type="disabled"}`。
但官方文档里那个 `extra_body={"thinking":...}` 是 **OpenAI SDK 层参数** ——
SDK 会把它摊平进 HTTP 请求体顶层。而适配器是**裸发** JSON 给 api.deepseek.com,
多包一层 `extra_body` 服务端根本不认,直接忽略。

⇒ 即使内核 thinking_enabled=false(发出 disable_thinking=true),
   也从来没真正关掉过思考,每次都走 thinking 模式,每次都撞 reasoning_content 规则。

改为顶层 `req.thinking = {type="disabled"}`。

### ② reasoning 从未被保存和回传

DeepSeek 规则(官方 thinking_mode 指南):请求**带 tools** 时,
所有历史轮的 reasoning_content 都必须传回;内核每轮都带工具定义。

而内核三层全丢:

    L1  ContextEvent 没有该字段;renderL1Messages 渲染 assistant 时不带
    L2  coldcontext 只切 Input/Response 两块,推理内容不进块
    L3  蒸馏原文只读 Input/Response/工具输出

修法(按用户裁定:L1/L2/L3 都要包含 reasoning):

    L1  ContextEvent.ReasoningContent + renderL1Messages 回传
        顺带:textForVector 纳入 reasoning —— 关键线索常只在思考里
        ("用户要的是 A 不是 B,所以先查 C"),只看 Response 会误裁
    L2  RoleReasoning 独立块(不并进 Response:回灌时要放回不同字段,
        合并会让思维链被当成正式回复发给模型)
    L3  coldcontextOriginalText 按**时序** input → reasoning → response

## 一个易错点:不能用 f.Resp.ReasoningContent

f.Resp 只在「本批有工具调用」时赋值(stepLLM 里 len(ToolCalls)==0 就 return)。
纯文本收尾轮拿到的是**上一次工具轮**的旧响应 —— 用它写进 L1 会把上一批的
思维链冒名顶替成本轮的(比缺失更坏)。所以单独一个 TurnReasoning,
由 stepLLM 在分支**之前**无条件写入。

## 适配器兜底(治存量)

内核修了,但**存量会话**的 context.json 是修复前写的,那些事件没这个字段。
不兜底就是死结:历史缺字段 ⇒ 400 ⇒ 会话永久不可用(重启也救不回,
缺的字段在磁盘上的旧记录里)。

transform_request 里:带 tools 时给每条 assistant 补 reasoning_content="",
已存在则不覆盖(不抹掉真实思考内容)。

## 顺手修的既有缺陷

零时间戳时 docBlocksForEntry 用 time.Now() 算块 ID,而 ColdEvent.Timestamp
留零值 ⇒ 哈希永久失配 ⇒ 内容就在那里却一个字段都读不回,且不报错不记日志。
线上因为 L1 总带时间戳没暴露。docEventsFromEntries 里先归一化时间戳。

## 验证

    生产实机(A2A 多轮工具调用,日志确认落到 deepseek 源直连):
      LLM fallback: trying provider "deepseek"
      response (12186ms, tools=[cmd_run plugin_start cmd_run])
      reasoning_content 400 = 0 次

    单元判据 16 条,全部变异验证(改回错误形态即红):
      coldcontext/test/reasoning_contract_test.go   5 条(L2 往返/独立块/
        无思考不造空块/特殊字符/InOut 兼容)
      agent/api/test/reasoning_protocol_test.go     3 条(顶层字段/无思考不发/
        带 tool_calls 仍发)
      lua/test/deepseek_reasoning_test.go           8 条(补空串/不覆盖真实值/
        无 tools 不补/补全部轮/不破其他字段/顶层 thinking/默认不加 thinking)

    go build / vet / gofmt / go test -race  全绿
2026-11-11 21:38:54 +08:00
aac11f6661 test: 按模块导出接口重写黑盒判据(批次 4:coldcontext / tracker / network)
3 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 15 条判据,只调导出接口,输入 → 可观察输出。

## internal/memory/coldcontext —— NewStore/Insert/Get/Query/Consume/Blocks

    TestInsertThenGet                 空 ID 自动补齐;Get 拿回内容;时间戳填好
    TestInsertExplicitIDKeepsIt       显式 ID 不被覆盖
    TestQueryReturnsRelevantDoc       按分词器词形召回相关文档
    TestConsumeIsOneShot              消费过的文档不再返回
    TestBlocksReflectsInsertedDoc     Blocks 输出可调用

**判据写错过一次并修正**:分词器按空白切词,文档里「发票重试」是一个
token,所以「发票 重试 账单」查不到而「发票重试」能查到 —— 实测探针
确认后按真实输入契约写。判据的输入必须能走到被测路径。

## internal/tracker —— NewTracker/Init/PreAction/PostAction/Rollback

    TestFreshTrackerHasNoChanges      新 tracker 无 changeset
    TestNewChangeSetCarriesAction     Action/ID/时间
    TestPostActionWithoutFileChangeIsNoop  无文件变更不凭空造 changeset
    TestRollbackLatestWithoutChangesReportsError
    TestRollbackClearsChangeSets      Rollback 后视图清空

不碰 t.changeSets、不假设 upper/merge 目录结构。

## internal/network —— NewMonitor/CheckOnce/Status/AggregateResult

    TestCheckOnceReachableServer      2xx → Reachable=true + URL/时间
    TestCheckOnceServerError          5xx → Reachable=false
    TestCheckOnceUnreachableEndpoint  连不上 → Error 非空,不 panic
    TestCheckOnceInvalidURL           非法 URL → 失败状态
    TestFreshMonitorStatusIsEmpty     初始状态为空

用 httptest 提供真实 HTTP 输入,不 mock client、不碰 m.status。

## 变异验证(改回旧实现必须红)

    coldcontext Insert 不补 ID       → ★ Insert 没有给空 ID 的文档补 ID
    network     5xx 判可达           → ★ 5xx 端点判可达
    tracker     无变更也造 changeset → ★ 无文件变更却 HasChanges=true

全部还原后复跑全绿。

## 验证

    go build ./...                            PASS
    go vet ./...                              PASS
    gofmt -l                                  干净
    python3 .github/scripts/check-ci-gates.py PASS
    go test ./... -count=1                    17 个测试包 / 100 包无测试 / 0 FAIL
    3 个新包 -race                            全绿
    源码目录 _test.go                         0
2026-11-09 12:56:13 +08:00
53cdc7f2a6 refactor(test): 肃清全部白盒测试 —— 源码目录零 _test.go,判据只走模块导出接口
## 用户裁定(本次唯一方向)

1. 「测试不应该面向过程而是面向模块的输入和输出」
2. 「每个模块下应该使用 test 目录搜集所有测试文件,而不是和源码混在一起」
3. 「现在 test 都超过源码数量了,这很明显不符合模块接口黑盒化的要求」
4. 「ast判包依赖?这不是工具链该做的吗?为什么在测试里」
5. 「抽行为判据?只需要面向模块输入接受模块输出就行」
6. 「不管ci,直接把所有白盒测试全清理了再回去一鼓作气改ci」

## 实测起点

    537 个测试文件 / 377 个源码文件 / 2853 条 Test
    523 / 537(97%)直接调用私有函数
    514 个是 in-package 白盒(package xxx,与源码同目录)
     88 个读 .go/.md 源码做断言
     94 个断言私有字段(f.Step / r.buf / a.sched …)
     38 个直调私有函数(stepLLM / runStage / classifyProviderError …)
    internal/agent/core: 140 test / 52 src

## 做了什么

### ① 删除 514 个 in-package 白盒测试

不做「抽行为判据」,也不做逐文件搬运。理由:Go 按目录分包,
`pkg/test/` 里写 `package pkg` 也看不到 unexported 符号 —— 所以
「挪进 test/」和「改成黑盒」是同一件事的两面。把 514 个白盒文件
搬走只会得到一堆编译不过的文件,或者为了迁就它们把内部全导出。

用户明确否定「抽」这个动作后,剩下 9 个 test/ 里的判据也一并
重新审视:凡是靠读源码/前端资源文本做断言的(dashboardJSSource /
FrontendAsset / GraphDataForVisual / SSEEventRing …)全部删除,
只留走模块**本来就有**的公开 API 的输入→输出判据:

    internal/agent/core/child/test/     Registry 的 Add/Get/Stop/Remove 输出
    internal/agent/core/test/           StageHost 的 Register/Run 输出
    internal/plugins/files/test/        工具 handler 的 {path} → {content/error}
    internal/plugins/webui/test/        HTTP 请求 → 响应(NewHandler/RegisterRoutes)
    internal/tfidf/test/                文本 → 向量/检索结果

现存 9 个测试文件全部在 `<module>/test/`,源码目录零 `_test.go`。

### ② 回退「为测试而导出」的接口

这些是前几轮为了把白盒判据挪出源码目录而导出的,属于白盒残留,
现在没有调用方了,全部收回私有:

    webui.FrontendAsset / DashboardPage        删除
    webui.GraphDataForVisual → graphDataForVisual
    webui.SSEEventRing/Record → sseEventRing/sseEventRecord(含字段)
    webui.NewSSEEventRing                       删除
    kstate.MountForTest                         删除
    security.ClassifyPrincipalForTest           删除

生产代码对它们的内部调用同步改回私有名,不改任何行为。

### ③ CI 一鼓作气改到位(用户指定顺序)

**删**:`整机启动图判据` 单列步骤(它跑的 bootgraph_test.go 已删),
以及 `check-ci-gates.py` 里盯着那个步骤的断言 —— 继续盯着一个
不存在的步骤就是永远红的空转。

**加两道构建期静态门**(原先这些约束错放在 go test 里):

- `测试归拢(源码目录不得有 _test.go)`
  `find internal cmd pkg providers deploy -name '*_test.go' -not -path '*/test/*'`
  非空即红。物理上挡住白盒回潮。
- `包依赖门(kstate 为叶子 / 底层包不反向依赖装配器)`
  用 `go list -deps` 判 8 组禁止依赖 + `go build core` 确认环不存在。
  这是**工具链**的职责,不该由 `go test` 兼职。

`check-ci-gates.py` 同步改为盯这两道门;ci.yml 里 13 处既有
>80 字符行一并折行(门禁标了 STOP,不留尾巴)。

### ④ 顺带(上一批未提交的)

`scripts/make-package.sh` 产物名 `HomeAgent-linux-*` →
`HologramAgent-linux-*`;清理 gitignored 构建产物
`build/ dist/ .go/ .npm-cache/` 共 20G(工作区 21G → 1.7G)。
运行契约(~/.homeagent/cli.sock、线上域名、兼容别名)未动。

## 验证

    go build ./...                     PASS
    go vet ./...                       PASS
    gofmt -l                           干净
    python3 .github/scripts/check-ci-gates.py   PASS
    go test ./... -count=1             5 个测试包全过 / 100 个包无测试 / 0 FAIL
    源码目录 _test.go 数              0
    测试文件数                         9(全部在 <module>/test/)

## 后续

按模块导出接口清单重写判据(不是从旧白盒里抽):
internal/agent/api、internal/memory、internal/security、internal/plugin、
internal/knowledge、cmd/* 等目前 0 判据,需要逐个列接口 → 写输入→输出判据
→ 变异验证。这是新一轮工作,本提交不含。
2026-11-09 11:13:56 +08:00
7c0eec1b2f docs: 第二层改名 Cold Context,清理四组已不存在的符号名
## 问题

README 与两份 ARCHITECTURE / OVERVIEW 仍在用旧称「Document 层」,
而第二层早已改名 cold context(工具名 `coldcontext_query` / `coldcontext_commit`)。

不止是层名。文档里引用的四个符号在代码里**一个都不存在**:

| 文档里写的 | 代码里实际 |
|---|---|
| `docStore.ContextToDoc` | `coldcontextStore.ContextToColdContext` |
| `FindColdDocs` | `FindColdContexts` |
| `docToTriples` | `coldDocToTriples` |
| `docStore.SetVectorizer` | (已无此调用,embedder 构造期注入) |

四处都用 `grep -rn "\b名字\b" --include=*.go` 确认过:旧名 0 处,新名各自 4 处。

## 改了什么

- `assets/docs/zh/ARCHITECTURE.md` 13 处、`en/ARCHITECTURE.md` 15 处
- `assets/docs/zh|en/OVERVIEW.md` 各 3 处
- README 两版:「文档层」→「冷上下文层」
- `assets/branding/mascot-xiaozhai-guide.md` 的丝带配色表(三色分别对应三层)

## 顺带修正层号

ARCHITECTURE 用的是 **L1 Context / L2 Cold Context / L3 Stable**,
我上一版 README 写成了 L0/L1/L2 —— 少了一层,且与架构文档对不上。已改为 L1/L2/L3。

## 顺带修掉断掉的交叉引用

`internal/agent/core/toolcall.go` 与 `recallcoldcontext_test.go` 的注释写
「ARCHITECTURE「记忆流转」② Document 层的读取分支」,指的正是刚改名的节。
不改就成了悬空引用。三个测试函数名(`TestContextToDoc` 等)指向已删函数,一并改名。

## 验证

- 全仓 `grep` 旧名:`*.md` / `*.go` 均 0 处(排除 build/third_party/.pi)
- `go build ./...` ✓
- `go test ./internal/memory/coldcontext/ ./internal/agent/core/` ✓
2026-10-31 11:39:13 +08:00
b850feaef2 feat: 段页式记忆流转、coldcontext 重命名与安全治理
记忆层:
- MemoryBlock 承载 FrameTS/PageSeq/PageKind/BlockRole/CallID
- coldcontext 归档与回填按完整页传递,工具调用参数和配对键不丢失
- 召回先估算总 token,超预算时按全局页相似度标记 inactive
- inactive 仅是 context 渲染期状态,不落盘、不进入 coldcontext,Prune 后重置
- Peek/确认/Remove 两阶段召回,失败保留冷上下文副本
- stablemem 记录 last_recalled_at,删块连带清理边

命名:
- internal/memory/document → internal/memory/coldcontext
- Doc/DocEvent/DocHit → ColdDoc/ColdEvent/ColdHit
- ContextToColdContext/RecentColdContexts/FindColdContexts
- 保留既有 JSON 标签、doc_ 文件前缀与 doc_count 状态键,兼容存量数据

安全治理:
- 工具策略门禁、安全配置、事件总线发布者身份与入站脱敏
- 声明式 SecurityAPI 与 QQ owner/guest profile
- 日志保留策略与相关 e2e/回归判据

验证:go build ./...、go vet ./...、go test ./...(62 包);
生产副本 e2e:7013 块迁移后 ID/向量/指纹 0 变化、0 缺失。
2026-10-29 23:53:48 +08:00