大模型工程化的自举迭代框架:从 Demo 到可持续生产系统的五层实践
大模型项目的最大陷阱不是模型不够强,而是做完 Demo 就停滞了。让一个 LLM 驱动的系统持续稳定运行,需要的不是更好的 prompt,而是一套”让模型能够优化自己工作过程”的工程体系。
引言:Demo 之后是什么
过去一年多,我用 AI 辅助编程从”新奇工具”逐步演变成了日常开发的核心环节1。在这个过程中反复出现一个规律:做一个能跑一次的 Demo 很简单,但让系统持续稳定运行几个月,难度是指数级上升的。
原因不难理解。Demo 只需要 model + prompt 就能出效果,但生产系统面临的是完全不同的约束:
- 上下文爆炸:多轮对话越长,token 成本越高,压缩后又丢失关键逻辑
- 流程不稳定:同样的 prompt 昨天还能用,今天就输出完全不同
- 成本失控:没有反馈循环,低效的交互模式被无限重复
- Agent 迷失:长期运行的 Agent 在复杂任务链中丢失上下文和决策依据
这套经验的本质,可以概括为一个闭环:
graph LR
A[手动验证] --> B[固化脚本]
B --> C[定时触发 / CI]
C --> D[自动收集反馈]
D --> E[分析 Debug 信息]
E --> F[蒸馏 / 优化模型或 prompt]
F --> A
style A fill:#90EE90
style B fill:#87CEEB
style C fill:#87CEEB
style D fill:#FFD700
style E fill:#FFD700
style F fill:#FF6347
让 LLM 不仅解决问题,还能持续优化解决过程本身——这就是”自举”的含义。
下面按成熟度从低到高,逐一拆解五层工程实践。
一、复杂流程固化:不要让 LLM 重复学习同一件事
最基础也最容易被忽略的一层。LLM 每次运行都是从零开始”思考”,但很多流程是稳定的、可重复的。反复让模型重新推导这些固定流程,既是浪费 token,也是引入不确定性。
核心原则:一旦某个流程跑通了三次以上,就把它从 prompt 里抽出来,固化成脚本或 API。
| 流程类型 | 固化方式 | 实例 |
|---|---|---|
| 稳定的 prompt 链 | Python 脚本 | 自动加载 CLAUDE.md,注入项目上下文 |
| 周期性测试流程 | Nightly 脚本 | 夜间批量跑回归测试用例2 |
| Agent 重启 / 恢复 | tmux + 脚本 | 自动重启 /loop 循环 |
| 外部调用 | FastAPI 封装 | 将 Agent 能力暴露为 HTTP API |
# 示例:定时自动化脚本
0 2 * * * /path/to/nightly_test.sh # 夜间测试
*/30 * * * * tmux send-keys -t claude "load claude.md" Enter # 定时刷新上下文
固化不仅仅是省 token。更重要的是,一旦流程变成脚本,它就进入了版本控制,可审计、可回归、可复用。这是从”人在驱动模型”到”系统在驱动模型”的第一步。
二、tmux 作为 Agent 运行时核心
这是整套框架里最独特的一层。大多数 LLM 工具链关注的是”调用 API 一次拿结果”,但长期运行的 Agent 需要的是一个持久化的运行时环境。tmux 恰好提供了这个能力。
核心模式:
# 创建持久化 Agent 会话
tmux new -s claude_agent
# 发送 prompt 而不打断会话
tmux send-keys -t claude_agent "analyze the latest logs" Enter
# 捕获输出到文件
tmux capture-pane -t claude_agent -p > latest_output.txt
为什么用 tmux 而不是直接调用 API?三个原因:
- LLM 保持长期状态:上下文、记忆、思维宫殿不因单次调用结束而丢失
- 自动化脚本可以”悄悄观测”:通过
capture-pane读取输出,而不干扰正在进行的推理 - 崩溃后可恢复:tmux session 独立于进程,即使客户端断开,Agent 仍在运行
sequenceDiagram
participant Script as 定时脚本
participant tmux as tmux Session
participant Agent as Claude Agent
participant FS as 文件系统
Script->>tmux: send-keys "nightly test"
tmux->>Agent: 传递指令
Agent->>Agent: 执行测试分析
Script->>tmux: capture-pane 捕获输出
tmux-->>Script: 输出内容
Script->>FS: 保存 latest_output.txt
Note over Script: 解析结果,决定下一步
tmux 在这里承担的角色,类似于容器运行时之于微服务——它提供进程隔离、生命周期管理和输入输出信道。这是把 LLM 当作”长期服务”而非”一次性函数调用”的关键基础设施。
三、Compact Conversation 卡点的工程解法
上下文爆炸是长期运行 Agent 面临的最棘手的工程问题。典型症状:
- 多轮对话后 token 消耗指数增长
/compact压缩后丢失关键决策逻辑- Agent 在压缩后”失忆”,行为偏离原定方向
解法是一个混合记忆策略,把不同生命周期的信息放在不同层级:
class HybridMemory:
def __init__(self):
self.short_term = [] # 当前对话窗口
self.memory_palace = MemoryPalace() # 结构化长时记忆
self.claude_memory = ClaudeCodeMemory() # 原生 memory 机制
def compact(self):
# 在压缩前自动提取关键决策
decisions = extract_important_decisions(self.short_term)
self.memory_palace.add(decisions)
# 再调用原生压缩
self.short_term = self.claude_memory.compress(self.short_term)
| 卡点症状 | 方案 | 工程实现 |
|---|---|---|
| 对话过长 | 思维宫殿 | 结构化记忆:summary/entities/relations 保存到 JSON |
| 压缩丢关键信息 | 定时脚本固化 | 每次 compact 前自动提取 debug 信息 |
| Agent 迷失 | Memory 机制 | Claude Code 内置 memory + 外部结构化存储 |
关键洞察:compact 不是纯压缩,而是一次”蒸馏”——把对话里的噪声压掉,把决策留下来。 这要求在压缩动作前后各加一层逻辑:压缩前提取,压缩后注入核心上下文。
四、埋点驱动的迭代:构建数据飞轮
模型本身不会告诉你它哪里做得不好。要让系统持续改进,需要在生产代码里埋入多层 debug 信息,形成反馈闭环。
Level 1 — 输入输出:
- raw_prompt
- llm_response
- latency / token_usage
Level 2 — 决策路径:
- which_functions_called
- compact_triggered_at
- retry_sequence
Level 3 — 业务结果:
- task_success (bool)
- retry_count
- hallucination_flag
三层埋点的设计遵循从”发生了什么”到”为什么发生”的递进:
graph TD
A["Nightly 测试失败"] --> B["查 L1 日志:输入输出"]
B --> C["查 L2 日志:决策路径"]
C --> D["定位某类错误高频"]
D --> E["蒸馏正确 case"]
E --> F["优化 prompt / 微调模型"]
F --> G["下一轮 Nightly 覆盖"]
style A fill:#FF6347
style D fill:#FFD700
style G fill:#90EE90
这套循环已经在实践中验证过有效。例如在做 Java task_server 到 Rust 的迁移项目中2,Nightly CI 测试失败 → 查 debug 日志 → 发现某类边界条件高频出错 → 把正确处理方法蒸馏到 prompt 模板 → 下一轮覆盖率提升。没有埋点,这个循环就断了。
五、用强类型驯服 LLM 的不确定性
LLM 输出天然 unstructured,但生产系统需要 structured。这两者之间的张力,是所有 LLM 工程化项目的终局问题。
Python 生态里常见的写法是:
if "action" in response:
do_something()
elif response.get("result"):
...
这种防御式编程在面对 LLM 的输出多样性时非常脆弱——模型换一个表达方式,if-else 就漏过去了。
Rust 的强类型系统提供了一种更可靠的方案:
// 强类型约束 LLM 输出结构
#[derive(Deserialize)]
struct LLMAction {
action_type: ActionType, // 枚举,非法值直接反序列化失败
parameters: ValidatedParams, // Option 强制处理缺失
}
// 而不是散落各处的 `if "action" in response`
强类型在这里的价值不是性能,而是把校验从运行时提到编译期:
ActionType枚举限定了合法动作空间,模型输出不在集合内 → 直接拒绝,不会悄悄执行错误逻辑Option<T>强制处理缺失字段,不会出现KeyError或NoneType的运行时崩溃- Schema 本身是可版本化的文档,团队对 LLM 输出的结构约定一目了然
具体应用场景包括:
- API 调用参数的 schema 校验
- Agent 之间的消息协议(防止幻觉渗透到下游)
- 部署脚本的类型安全(避免”模型乱编文件路径”)
工程化成熟度总结
| 实践 | 解决的问题 | 成熟度 | 复用性 |
|---|---|---|---|
| 流程固化(脚本/API) | 避免 LLM 重复学习 | ⭐ 基础 | 极高 |
| tmux 运行时 | Agent 持久化的工程基础 | ⭐⭐ 进阶 | 高(独特点) |
| Compact 卡点解法 | 控制 token 成本 + 保留关键信息 | ⭐⭐⭐ 高阶 | 高 |
| 埋点驱动迭代 | 形成数据飞轮,持续改进 | ⭐⭐⭐ 高阶 | 极高 |
| 强类型驯服不确定性 | 对抗 LLM 输出不可靠 | ⭐⭐⭐⭐ 前瞻 | 中高 |
自举的本质
回到最核心的那个闭环图。五层实践的共性不是”让模型更强”,而是把人的工程判断逐步沉淀为系统能力:
- 手动跑通 → 写成脚本(自动化)
- 脚本稳定 → 接入 CI(持续化)
- CI 出问题 → 埋点收集数据(可观测)
- 数据积累 → 分析 pattern → 优化 prompt 或蒸馏模型(自优化)
每一步都是在把”人驱动模型”的占比降低一点,把”系统驱动模型”的占比提高一点。这就是自举的含义——不是一次性配置好就完事,而是构建一套能持续自我改进的机制。
这和软件工程的经典原则并无不同:自动化、可观测、持续集成、反馈闭环。只是这一次,优化的对象不是代码,而是”模型理解代码、生成代码、维护代码”的过程本身。