对应代码:
app/agent/memory.py、app/observability/tracer.py
这三件事看起来是"锦上添花",实际决定了 Agent 能不能上生产。
一、记忆:窗口 + 摘要
问题
多轮对话时如果每轮都把全部历史塞进 Prompt:
- 贵:token 线性增长
- 慢:首 token 延迟越来越高
- 笨:研究显示长上下文存在"中间遗忘"(lost in the middle)
方案
markdown
最近 12 轮原文(保证指代可解析)
+
更早内容的 LLM 摘要(保证长期一致性)
python
class ConversationMemory:
def load(self, thread_id):
return self.backend.summary(thread_id), self.backend.history(thread_id, window)
def save_turn(self, thread_id, user_msg, assistant_msg):
self.backend.append(...)
self._maybe_summarize(thread_id) # 超过 16 条才触发
def _maybe_summarize(self, thread_id):
delta = "\n".join(f"{m['role']}: {m['content'][:300]}" for m in history)
new_summary = llm.chat([{"role":"user","content":SUMMARY_PROMPT.format(old=old, delta=delta)}])
摘要 Prompt 明确要求保留三件事:
1) 用户关注的对象(项目名/时间范围/指标)
2) 已给出的结论
3) 待办或下一步
压缩后只保留最后一轮,避免旧消息重复计入。
三种后端
| 后端 | 适用 | 说明 |
|---|---|---|
memory |
Demo / 单测 | 进程内 |
sqlite |
单机部署 | 落盘,重启不丢 |
redis |
在线服务 | 带 TTL(默认 7 天) |
追问:为什么不用向量记忆?
向量记忆适合"检索很久以前的事实",但对"上一步我说了什么 "这种时序指代效果差, 且每轮都要额外做 embedding。 生产上常见做法是两者结合:摘要 + 窗口做主线,向量记忆做事实补充。
二、可观测:自建 Tracer
为什么不用 LangSmith
企业内网通常不允许把 prompt 出网。 所以自建了一套,同时保留适配位(设置环境变量即可并行上报)。
数据模型
python
@dataclass
class Span:
trace_id, span_id, name, parent_id
start_ms, end_ms, status
attributes: Dict # 节点自定义属性:model、tokens、sql、rows...
@dataclass
class Trace:
id, query, created_at, meta
spans: List[Span]
tokens: {"prompt": n, "completion": n, "total": n}
llm_calls: int
用起来就是一个上下文管理器
python
with tracer.span(trace_id, "node.plan"):
...
with tracer.span(trace_id, "text2sql", attempt=attempt) as sp:
sp.attributes["raw_sql"] = raw_sql[:300]
落盘 + 实时推送
- 落盘 :
./.cache/traces/{trace_id}.json,可离线分析、可回放 - 实时 :
tracer.emit(event)→ 事件推给 SSE → 前端"执行链路时间线"
python
def add_listener(self, fn): self._listeners.append(fn)
def emit(self, event):
for fn in list(self._listeners):
try: fn(event)
except Exception: pass # 监听器异常不能影响主流程
排查问题时的实际用法
bash
答错了 → 打开 /api/traces/{id}
→ 看哪个 span 耗时异常
→ 看 plan 拆得对不对
→ 看检索命中了哪些 chunk(分数多少)
→ 看工具返回了什么
→ 看 reflect 判断"够了"是否合理
→ 看最终 prompt 上下文
没有这套东西,Agent 出问题时只能靠猜。
三、Token 与成本控制
统计
python
tracer.record_llm(trace_id, prompt_tokens, completion_tokens, model=resp.model)
前端右上角实时显示累计 token。
六个降本手段(项目里都实现了)
| 手段 | 做法 |
|---|---|
| ① 检索只喂 top-5 | 不把整篇文档塞进去 |
| ② 记忆压缩 | 窗口 + 摘要,不塞全量历史 |
| ③ 重排默认零调用 | heuristic;必要时才上 LLM rerank |
| ④ 迭代上限 | 防死循环烧钱(最重要!) |
| ⑤ 规划用小模型 | Prompt 里已预留模型切换位(LLM_FALLBACK_MODELS) |
| ⑥ 流式输出 | 用户可提前中断,减少无效生成 |
一次真实调用的开销
ini
问:本月各项目成本排名前 3 是多少?
prompt=3000 completion=509 total=3509
(含 2 次规划 + 2 次 SQL 生成 + 1 次反思 + 1 次回答)
这个数字本身也是面试素材:你能说清一次问答到底花多少 token, 比空谈"我们做了成本优化"有说服力得多。
四、三者的关系
arduino
记忆 ──▶ 决定"喂给模型什么" ──▶ 影响成本
可观测 ─▶ 记录"花了多少、哪里慢" ──▶ 指导优化
护栏 ──▶ 决定"能不能执行" ──▶ 控制风险
这三件事 + 前面的 RAG 和编排,才构成一套能上生产的 Agent 系统, 而不是一个"接了 API 的聊天 Demo"。