From 03b4fdabaf665694b2ebc7a6e766e33b40cdb76c Mon Sep 17 00:00:00 2001 From: weichen Date: Sat, 10 Oct 2026 18:11:24 +0000 Subject: [PATCH] Restrict the source tag to material reads; never tag the model's own files Why: The first E4 live comparison regressed badly (toolFailures 10->24, toolCalls 63->166) and the instance transcript shows the loop: tagging kernel read_file/grep results put a trailing [src_N] line on the model's own workspace files, the model treated it as external tampering, and re-read/re-edit cycles cascaded into the read-before- write and file-changed guards. Why: The ledger only ever needed SOURCES - searches always, and workspace reads only under inputs/ (the upload material area). Grep cannot distinguish materials from self-produced files, so it stays untagged. Evidence: go test ./... green; new anchor TestEvidenceLedger OnlyTagsMaterialReads pins the inputs/-only rule; the re-run live comparison follows on the Dev side. Impact: evidenceTools loses read_file/grep; isEvidenceRead gates workspace reads to inputs/; everything else (search tags, escalation reminders, prompts) unchanged. Refs: Dev task feature/agent-e4-escalation-ledger (fix-up of #131). --- internal/runtime/e4_escalation_test.go | 15 +++++++++++++++ internal/runtime/factcheck.go | 25 ++++++++++++++++++++++--- internal/runtime/llm_direct.go | 5 +++-- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/internal/runtime/e4_escalation_test.go b/internal/runtime/e4_escalation_test.go index a5eef0e..4d1bca2 100644 --- a/internal/runtime/e4_escalation_test.go +++ b/internal/runtime/e4_escalation_test.go @@ -2,6 +2,7 @@ package runtime import ( "context" + "encoding/json" "strings" "testing" @@ -104,3 +105,17 @@ func TestEvidenceLedgerSkipsNonEvidenceTools(t *testing.T) { t.Fatalf("non-evidence tool must not enter the ledger: %.80s %+v", res.Content, st.ledger) } } + +// 材料读(inputs/)打标;模型自产文件的读与 grep 不打——尾注会污染 +// read→edit→build 编辑循环(E4 首轮 eval 实测教训,回归锚点)。 +func TestEvidenceLedgerOnlyTagsMaterialReads(t *testing.T) { + if !isEvidenceRead("read_file", json.RawMessage(`{"path":"inputs/年报.docx.md"}`)) { + t.Fatal("inputs/ read must count as evidence") + } + if isEvidenceRead("read_file", json.RawMessage(`{"path":"方案.md"}`)) { + t.Fatal("own-workspace read must NOT count as evidence") + } + if isEvidenceRead("grep", json.RawMessage(`{"pattern":"营收"}`)) { + t.Fatal("grep must never count as evidence") + } +} diff --git a/internal/runtime/factcheck.go b/internal/runtime/factcheck.go index d75be9b..8d21834 100644 --- a/internal/runtime/factcheck.go +++ b/internal/runtime/factcheck.go @@ -1,6 +1,7 @@ package runtime import ( + "encoding/json" "fmt" "regexp" "strings" @@ -242,14 +243,32 @@ type evidenceEntry struct { Text string } -// evidenceTools 登记进 source_id 账本的证据类工具(读检索面)。 +// evidenceTools 无条件登记进 source_id 账本的证据类工具(检索面)。 +// 内核工作区 read_file 不在此列:它也读模型自己的工作文件——给自身文件 +// 的读结果尾注【src_N】会污染 read→edit→build 的编辑循环(实测 E4 首轮 +// eval:模型视尾注为文件被外改动,反复重读重试,工具失败 10→24、调用 +// 63→166)。仅当读取 inputs/ 材料时才登记(isEvidenceRead)。 var evidenceTools = map[string]bool{ "knowledge_search": true, "web_search": true, "tavily_search": true, "fs_read_file": true, - "read_file": true, // 内核工作区读(直连路径实际使用的名字) - "grep": true, +} + +// isEvidenceRead 判定一次工作区读是否为「材料读」:仅 inputs/(上传材料 +// 落点,环境段列出)之下的 read_file 计入账本;grep 无法区分材料与自产 +// 文件,一律不计。 +func isEvidenceRead(name string, args json.RawMessage) bool { + if name != "read_file" { + return false + } + var obj map[string]string + if err := json.Unmarshal(args, &obj); err != nil { + return false + } + p := strings.TrimSpace(obj["path"]) + p = strings.TrimPrefix(p, "./") + return strings.HasPrefix(p, "inputs/") || strings.HasPrefix(p, "/inputs/") } // escalationFailureThreshold 是连续工具失败的换路提醒阈值(R4)。 diff --git a/internal/runtime/llm_direct.go b/internal/runtime/llm_direct.go index 4a0bfbc..c2a7b0d 100644 --- a/internal/runtime/llm_direct.go +++ b/internal/runtime/llm_direct.go @@ -174,8 +174,9 @@ func (l *LLMRunner) runDirect(rc RunCtx, stream agentllm.StreamFunc, model strin reg.Register(directTool{name: name, run: func(ctx context.Context, args json.RawMessage) agenttools.Result { res := wsReg.Execute(ctx, name, args) st.noteKernelResult(ctx, res) - // E4:证据类工作区工具(read_file/grep)同样登记账本并尾注来源标记。 - if !res.IsError && evidenceTools[name] && strings.TrimSpace(res.Content) != "" { + // E4:工作区读仅在 inputs/ 材料读时登记账本(isEvidenceRead; + // 自产文件的读结果不打标,防编辑循环被尾注污染——实测教训)。 + if !res.IsError && (evidenceTools[name] || isEvidenceRead(name, args)) && strings.TrimSpace(res.Content) != "" { if tag, ok := st.tagSource(name, res.Content); ok { res.Content += "\n【" + tag + "】" }