你在 Go 里做 Agent,最容易翻车的不是推理能力,而是“记忆”失控:上下文越来越长、账单越来越高、回答却越来越飘。

这篇给你一个可落地的三层方案:

  • L1:短期会话上下文(秒级,强相关)
  • L2:中期摘要记忆(分钟级,压缩)
  • L3:长期检索记忆(天级,向量索引)

一、先定规则:什么进上下文,什么只进索引

很多系统把所有历史对话都塞进 Responses API,这是最贵也最不稳定的做法。

建议直接定硬规则:

  1. L1(短期)只保留最近 N 轮:例如最近 8~12 轮。
  2. 超过阈值就摘要:把旧对话压缩成结构化摘要放 L2。
  3. 事实记忆走检索:用户偏好、项目约束、历史决策进入 L3,不直接长期占 token。
  4. 每次请求都有预算上限:超预算就截断或降级,不要“硬冲”。

二、Go 数据结构:把记忆分层写死到代码里

type Message struct {
    Role      string    `json:"role"`
    Content   string    `json:"content"`
    Timestamp time.Time `json:"ts"`
}

type SessionMemory struct {
    SessionID      string
    ShortWindow    []Message // L1
    RollingSummary string    // L2
    Budget         TokenBudget
}

type TokenBudget struct {
    MaxInputTokens int
    ReserveOutput  int
    HardCapUSD     float64
}

核心原则:先预算,后拼装

三、请求组装顺序(最重要)

每次调用 Responses API 时,按下面顺序组装输入:

  1. System 指令(固定)
  2. L2 摘要(简短、结构化)
  3. L3 检索结果(Top-K,带来源)
  4. L1 最近消息窗口
  5. 当前用户输入

这样做的好处:

  • 语义稳定(先给“全局记忆”,再给“局部上下文”)
  • 成本可控(L3 只取 K 条,L1 有窗口)
  • 易调试(每层可单独观测)

四、可直接抄的“上下文爆炸”治理代码

func BuildInput(mem SessionMemory, retrieved []string, userInput string) []Message {
    var input []Message

    // 1) 固定系统指令
    input = append(input, Message{Role: "system", Content: "You are a precise coding assistant."})

    // 2) L2 摘要
    if mem.RollingSummary != "" {
        input = append(input, Message{Role: "system", Content: "Session summary:\n" + mem.RollingSummary})
    }

    // 3) L3 检索记忆(限制 topK)
    topK := min(4, len(retrieved))
    if topK > 0 {
        input = append(input, Message{Role: "system", Content: "Retrieved memory:\n" + strings.Join(retrieved[:topK], "\n---\n")})
    }

    // 4) L1 最近窗口
    window := tail(mem.ShortWindow, 10)
    input = append(input, window...)

    // 5) 当前用户输入
    input = append(input, Message{Role: "user", Content: userInput})

    return input
}

五、成本封顶:用“两道闸门”防止账单失控

闸门 A:请求前 token 预估

  • 估算本次输入 token(可用 tiktoken 近似或历史均值)
  • estimated_input + reserve_output > MaxInputTokens
    • 先裁 L1
    • 再压 L2
    • 最后降 L3 的 topK

闸门 B:会话级美元硬上限

func GuardCost(sessionCostUSD, hardCapUSD float64) error {
    if sessionCostUSD >= hardCapUSD {
        return fmt.Errorf("cost cap reached: %.2f/%.2f", sessionCostUSD, hardCapUSD)
    }
    return nil
}

超过上限后返回可解释错误(比如“本会话已达预算上限,请开启新会话或降低上下文”),别悄悄失败。

六、长期记忆(L3)别当垃圾桶

L3 只收这三类:

  1. 稳定偏好:语言偏好、输出风格、禁区。
  2. 关键事实:项目路径、版本约束、依赖关系。
  3. 历史决策:为什么选 A 不选 B(带时间和依据)。

不要存:

  • 临时闲聊
  • 纯情绪表达
  • 已过期上下文

定期清理策略建议:

  • 7 天无命中降权
  • 30 天无命中归档
  • 明显冲突事实触发人工复核

七、线上观测指标(不做就等着事故)

至少打这 6 个指标:

  • agent_input_tokens_p95
  • agent_output_tokens_p95
  • memory_retrieval_hit_rate
  • summary_compression_ratio
  • response_latency_p95
  • cost_usd_per_session

hit_rate 下降且 input_tokens 上升,通常说明:检索噪声大 + 摘要质量差。

八、排障清单(5 分钟定位)

  1. 回答跑偏:先看 L3 是否召回了过期事实。
  2. 成本暴涨:看 L1 窗口是否失效、L2 是否过长。
  3. 延迟升高:看 topK 是否被误配到 10+。
  4. 重复回答:检查摘要是否丢了“已执行动作”。

总结

Go + OpenAI Responses 的 Agent 想稳定上线,关键不是“记得越多越好”,而是分层、限流、可观测

一套够用的默认值:

  • L1 最近 10 轮
  • L2 摘要 300~500 tokens
  • L3 topK=4
  • 每会话硬上限 $0.5~$2(按业务价值调)

先把这套跑起来,你的成本曲线和故障率会比“全量上下文硬塞”好看得多。