Files
weichen 21b8461525
ci / vet-and-test (pull_request) Successful in 43s
Add R4 escalation reminders and the structured source-id evidence ledger (E4)
Why: The plan's E4 remainder under the MiniMax-only constraint: the
harness must detect the model going in circles and inject a
change-of-strategy instruction (R4 - the model cannot be trusted to
notice its own repeat-fail loop), and number sourcing needs addressable
evidence: tool results carry [src_N|tool] tags so "found in materials"
becomes "found in source src_N" for both the model and the fact-check
notice.

Why: Failure counting lives in one place (onEvent end, keyed by the
slot status) because tool execution is parallel; the ledger and tag
append are serialized by the same mutex, and escalation reminders fire
once per condition streak - following the dual-speed nudge finding that
MiniMax ignores vague reminders but follows concrete instructions.

Evidence: go test ./... green; new anchors TestEscalationOnConsecutive
ToolFailures, TestEscalationOnZeroHitSearches, TestEvidenceLedger
TagsSource, TestEvidenceLedgerSkipsNonEvidenceTools. Live before/
after comparison runs on the Dev side per ruling 6.

Impact: knowledge/web/tavily searches, fs_read_file and the workspace
read_file/grep results now end with a source tag and enter the run
ledger (2k-char head); two consecutive tool failures or two empty
search results inject a concrete change-of-strategy reminder;
base.deliverables v7 and agent-sub-research v3 explain the tags;
fact-check notice points at src results. Non-evidence tools and
failures are unchanged.

Refs: proposal 2026-10-10-agent-delegation-routing-sandbox.md (E4,
R4); Dev task feature/agent-e4-escalation-ledger.
2026-10-10 17:41:38 +00:00

126 lines
4.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package runtime
import (
"context"
"strings"
"testing"
core "agent-system-core"
)
// 事实核查:交付物里材料找不到出处的百分比被抽出并组成反馈;找到的不报。
func TestUnverifiedFactsAndNotice(t *testing.T) {
material := "目标是准确率 85%以上。截止 10月12日。团队共 12 人。"
artifact := "准确率达到 95%,截止 10月20日,覆盖 3,500 用户,目标 85%。"
got := UnverifiedFacts(artifact, material, factExemptions())
joined := strings.Join(got, ",")
for _, want := range []string{"95%", "10月20日", "3,500"} {
if !strings.Contains(joined, want) {
t.Fatalf("missing %q in %v", want, got)
}
}
if strings.Contains(joined, "85%") {
t.Fatalf("sourced figure must not be reported: %v", got)
}
notice := factCheckNotice(got)
if !strings.Contains(notice, "95%") || !strings.Contains(notice, "改成材料或来源结果") {
t.Fatalf("notice malformed: %q", notice)
}
if UnverifiedFacts("", material, nil) != nil || UnverifiedFacts(artifact, "", nil) != nil {
t.Fatal("empty sides short-circuit")
}
}
// 端到端:终轮对带未核实数字的交付物给一轮修正;修正后(重新登记干净版本)放行。
func TestFactCheckContinuationOnce(t *testing.T) {
rc := &fakeRunCtx{
ctx: context.Background(),
prof: core.AgentProfile{Model: core.ModelRoute{Chat: "test-model"}},
verify: true,
execArtifact: &core.Artifact{ID: "art_0123456789ab", Name: "报告.md"},
execArtifactContent: []byte("# 报告\n\n准确率 95%。"),
}
// 第一轮:登记带编造数字的工件 -> 事实核查续跑一次;第二轮:干净版本通过。
turns1 := padTurns(
textDeltaTurn("", toolCall("make_report", `{"content":"准确率 95%"}`)),
textDeltaTurn("写好了。"),
)
turns2 := padTurns(
textDeltaTurn("材料里是 85%。"),
)
r := &LLMRunner{Stream: scriptStream(padTurns(turns1...), nil), VerifyStream: passedStream()}
if err := r.Execute(rc); err != nil {
t.Fatalf("Execute: %v", err)
}
sawFact := false
for _, p := range rc.assistant {
if p.Check != nil && !p.Check.Passed && strings.Contains(p.Check.Reason, "事实核查") {
sawFact = true
}
}
if !sawFact {
t.Fatalf("fact check continuation note missing: %+v", rc.assistant)
}
_ = turns2
}
// ---- E3:标注感知豁免 ----
func TestMarkedAsEstimate(t *testing.T) {
cases := []struct {
name, text, tok string
want bool
}{
{"前置标注", "目标完成率 85%,分三个阶段推进", "85%", true},
{"后置标注", "完成率 85%(预计)", "85%", true},
{"裸数字", "当前完成率 85%,尚有差距", "85%", false},
{"邻域外有标注词", "目标是建成样板。完成率 85%", "85%", false},
{"多次出现其一标注", "实际 70%。目标 85% 年内达成", "85%", true},
{"推测标注", "用户规模约 120,000(推测)", "120,000", true},
}
for _, c := range cases {
if got := MarkedAsEstimate(c.text, c.tok); got != c.want {
t.Errorf("%s: MarkedAsEstimate(%q) = %v, want %v", c.name, c.tok, got, c.want)
}
}
}
func TestUnverifiedFactsMarksPass(t *testing.T) {
material := "材料:上半年营收 3,200 万元。"
// 「目标 85%」已标注 → 放行;裸 95% → 仍拦。
artifact := "上半年营收 3,200 万元,达成率目标 85%,试点扩面后裸数字 95%。"
got := UnverifiedFacts(artifact, material, factExemptions())
if len(got) != 1 || got[0] != "95%" {
t.Fatalf("unverified = %v(应只拦未标注的 95%%)", got)
}
}
// ---- E3:两轮修正的轮次逻辑 ----
func TestFactCheckGateRounds(t *testing.T) {
st := &directRun{}
first := []string{"95%", "10月20日"}
// 第一轮:拦截并给修正通知。
notice, cont := st.factCheckGate(first)
if !cont || !strings.Contains(notice, "95%") || strings.Contains(notice, "最后一次拦截") {
t.Fatalf("round1: cont=%v notice=%q", cont, notice)
}
// token 完全没变(模型没理会):不再拦——重复拦截只挤占共享续写预算。
if _, cont := st.factCheckGate(first); cont {
t.Fatal("unchanged tokens must pass through")
}
// token 有变化(改了一个、残留一个):第二轮升级为「最后一次拦截」。
notice, cont = st.factCheckGate([]string{"10月20日"})
if !cont || !strings.Contains(notice, "最后一次拦截") || !strings.Contains(notice, "目标") {
t.Fatalf("round2: cont=%v notice=%q", cont, notice)
}
// 轮数用尽:放行。
if _, cont := st.factCheckGate([]string{"7%"}); cont {
t.Fatal("rounds exhausted must pass through")
}
if st.factCheckRounds != 2 {
t.Fatalf("rounds = %d", st.factCheckRounds)
}
}