与系列统一 :本文 Harness = LLM Agent 运行时治理层 (见 综述)。Redis 不替代 Harness,而是为 状态、限流、队列、协调、缓存 提供高性能底座。
示例语言 :Python 3.10+,redis>=5.0(redis-py)。正文业务片段统一用变量r;§1 连接池 / Sentinel / Cluster;§14 生产必备:TLS、ACL、Key 规范、监控告警、容量与持久化(开篇那句 checklist 的展开版)。
文档目录(建议阅读顺序)
| 章 | 主题 |
|---|---|
| §0 / §0b | 价值与场景自检表 |
| §1 | 连接池、Sentinel、Cluster |
| §2--§12 | 会话、锁、幂等、限流、Stream、Pub/Sub、缓存、HITL... |
| §6 | 重点:XREAD / XREADGROUP 全集 |
| §10b--§10c | 延迟队列、Pipeline |
| §17--§22 | 前后端、端云、流式、定时、发布消费、杂项 |
| §13 | Harness 模块映射 |
| §14 | 生产:TLS、ACL、监控、Key 规范、Checklist |
| §15 | 本地 Docker |
| §16 | 竞品与组合(§16.0 速查) |
| §23 | 延伸阅读 |
说明:§13--§16 在文件中位于 §22 之后,是为避免打断代码主题块;按上表顺序读即可。
0. 为什么 Agent 工程里常把 Redis 当「大杀器」
智能体系统有三个共性:高并发短请求 、多步长会话 、多进程/多实例协作。关系型数据库擅长事务与复杂查询,但在以下场景往往过重或太慢:
| 诉求 | Redis 擅长点 |
|---|---|
| 会话态、工具中间结果、checkpoint | 毫秒级读写;Hash / JSON(RedisJSON) |
| 全局限流、Token 预算、熔断计数 | 原子 INCR + EXPIRE;滑动窗口(ZSET) |
| 多 Agent 抢任务、防重复执行 | SETNX 分布式锁;XREADGROUP + XACK |
| 实时步进推前端 / 审计旁路 | XREAD + BLOCK(Fan-out,各自 last_id) |
| 人机协同审批、事件总线 | List / Stream;Pub/Sub |
| 语义缓存、Embedding 元数据索引 | String + TTL;可选 RediSearch / Vector |
Redis(横切能力)
Agent Harness
调用方
用户 / Webhook
Orchestrator
路由 / 策略 / 校验
LLM
Tools
Session / Checkpoint
Lock / Idempotency
Queue / Stream
Rate / Budget
Cache
定位一句话 :模型负责「想」,Harness 负责「做」;Redis 负责让「做」在 多实例、可恢复、可限流 的前提下依然够快。
0b. 应用覆盖全景(前后端 · 端云 · 队列 · 流式 · 定时 · 发布消费 · ...)
下面是一张 自检表 :左列是智能体系统里常见工程维度,中列是 Redis 典型用法,右列指向本文章节。✓ 表示文内已有可运行示例;△ 表示有要点/思路,§17--§22 补全代码。
| 维度 | 典型场景 | Redis 能力 | 本文 |
|---|---|---|---|
| 前后端 | 登录会话、run_id 绑定、防刷 | String/Hash + TTL | §17 ✓ |
| 前后端 | 浏览器看 Agent 步进(SSE/WebSocket) | Stream XREAD BLOCK |
§6.3、§17 ✓ |
| 前后端 | 上传大文件/工具产物前的短期凭证 | String SETEX |
§17 ✓ |
| 前后端 | 用户级 QPS / 并发 Run 上限 | ZSET 滑动窗口 / INCR | §5、§17 ✓ |
| 端云一体 | 手机/边缘端 Agent 与云端状态同步 | Hash 版本号 + Stream 指令下行 | §18 ✓ |
| 端云一体 | 离线排队、联网后批量上报 | List / Stream 本地队列 → 云 XADD | §18 ✓ |
| 端云一体 | 设备在线、最后心跳 | String/ZSET last_seen |
§18 ✓ |
| 队列 | 轻量任务、单消费者 | List BRPOP |
§6.1 ✓ |
| 队列 | 多 Worker 竞争、ACK、重试 | Stream XREADGROUP |
§6.6--6.8 ✓ |
| 队列 | 优先级、延迟执行 | ZSET + Stream / ZSET 时间轮 | §10b、§20 ✓ |
| 队列 | 死信(多次失败) | 独立 Stream / List DLQ |
§22 ✓ |
| 流式 | LLM Token 增量推前端 | Stream XADD + 前端 XREAD |
§19 ✓ |
| 流式 | 工具 stdout 行级推送 | Stream / Pub/Sub | §6、§19 ✓ |
| 定时任务 | Cron 扫描到期 Run、清理僵尸会话 | ZSET score=执行时间 | §10b、§20 ✓ |
| 定时任务 | 租约/心跳超时回收 | TTL + 调度器 ZRANGEBYSCORE |
§11、§20 ✓ |
| 发布 + 消费 | 广播取消、配置变更 | Pub/Sub PUBLISH |
§7、§21 ✓ |
| 发布 + 消费 | 至少一次、可回溯 | Stream XADD/XREAD |
§6 ✓ |
| 发布 + 消费 | 一条任务只处理一次 | Stream XREADGROUP |
§6.6 ✓ |
| 协调 | 分布式锁、幂等 | SET NX、Lua |
§3、§4 ✓ |
| 连接/部署 | 连接池、Sentinel、Cluster | ConnectionPool / RedisCluster |
§1、§14.1 ✓ |
| 生产安全 | TLS、ACL、Key 规范、监控 | rediss://、ACL 用户、Exporter |
§14 ✓ |
| 竞品/组合 | Kafka、Temporal、PG、向量库 | 热路径 Redis + 冷路径分工 | §16.0、§16 ✓ |
| 协调 | 集群单点 Leader 扫 pending | SET NX 租约 |
§22 ✓ |
| 缓存 | 相同 Prompt 省 Token | String + TTL | §8 ✓ |
| 缓存 | RAG 片段 / 工具结果 | Hash / String | §8、§22 △ |
| 多租户 | 配额、隔离 Key 前缀 | {tenant}: Hash Tag |
§1.4、§14.2、§22 ✓ |
| 人机 | 审批、超时 | List + ZSET | §9 ✓ |
| 可观测 | QPS、工具失败率 | INCR、HyperLogLog | §12、§22 ✓ |
| Serverless | 函数无状态,会话放 Redis | Hash Checkpoint | §2、§22 ✓ |
| 安全 | 密钥不下发模型,短期句柄 | String 指针 + TTL | §17、§22 ✓ |
Redis
云端 Backend
前端 / 边缘
HTTPS SSE
XREAD 代理 tail
同步 checkpoint
上行 XADD
XREADGROUP
可选 SUBSCRIBE
Web / App
边缘 Agent
API / BFF
Orchestrator
Agent Workers
会话 / 配额
Stream 事件与队列
Pub/Sub 广播
ZSET 定时 / 优先级
缓存 / 锁
怎么读这张表:
- 前后端 :Redis 几乎总在 云端 ;浏览器不直连 Redis(安全),由 BFF/API 用
XREAD转 SSE 或 WebSocket。 - 端云一体 :边缘只保留 小 checkpoint + 离线队列;真相源在云端 Stream/Hash。
- 队列 vs 流式 vs 发布消费 :任务用 消费者组 ;旁路观测用
XREAD;纯广播用 Pub/Sub(允许丢)。 - 细节示例见 §17--§22 ;Stream 命令全集见 §6。
1. 连接方式:单连接、连接池、Sentinel、Cluster
后文所有 r.xxx() 均假设 r 来自 get_redis() (进程内单例)。
开发 可用单连接;生产 默认 连接池 ;高可用 用 Sentinel 或 Redis Cluster。
高可用
生产推荐
开发 / 单测
本地
redis.Redis 单连接
ConnectionPool
- Redis 客户端
Sentinel 故障转移
Cluster 分片
API / Agent Worker
1.0 公共:Key 命名(概要)
完整生产规范见 §14.2(前缀、TTL、禁止项、按实体分类表)。代码里建议统一走工厂函数,避免手写拼接:
python
import json
import os
import time
import uuid
from functools import lru_cache
from typing import Any
import redis
from redis.cluster import RedisCluster
ENV = os.getenv("HARNESS_ENV", "dev") # dev | staging | prod
def key(*parts: str) -> str:
"""harness:{env}:... 见 §14.2"""
return ":".join(["harness", ENV, *parts])
| 规范 | 说明 |
|---|---|
| 统一前缀 | harness:{env}:{tenant?}:{实体}:...,便于 SCAN、按租户清理、监控聚合 |
| 必设 TTL | 会话、缓存、锁、幂等键都应有过期,避免「僵尸 Key」 |
| 值序列化 | 小对象用 JSON;大对象考虑压缩或只存 object storage 的指针 |
| 集群 | 多 Key 事务 / Lua 多 key 须 同一 hash slot → 使用 Hash Tag {tenant}:...(见 §1.4) |
1.1 单连接(仅本地调试)
python
# ❌ 生产反模式:每个 HTTP 请求都 Redis(host=...) 一次 → 连接风暴
def redis_single() -> redis.Redis:
return redis.Redis(
host=os.getenv("REDIS_HOST", "127.0.0.1"),
port=int(os.getenv("REDIS_PORT", "6379")),
password=os.getenv("REDIS_PASSWORD") or None,
db=int(os.getenv("REDIS_DB", "0")),
decode_responses=True,
socket_timeout=5,
socket_connect_timeout=2,
)
1.2 连接池(生产默认)
redis-py 的 Redis 客户端自带连接池 :同一个 Redis 实例复用池内 TCP 连接;多线程/多协程共享该实例即可(勿每线程 new 一个池)。
python
@lru_cache
def get_redis_pool() -> redis.Redis:
"""
进程内单例。Uvicorn/Gunicorn 多 worker = 每进程一个池,属正常。
可用 REDIS_URL 或 REDIS_HOST/PORT。
"""
url = os.getenv("REDIS_URL")
if url:
pool = redis.ConnectionPool.from_url(
url,
max_connections=int(os.getenv("REDIS_POOL_MAX", "50")),
decode_responses=True,
health_check_interval=30,
retry_on_timeout=True,
)
else:
pool = redis.ConnectionPool(
host=os.getenv("REDIS_HOST", "127.0.0.1"),
port=int(os.getenv("REDIS_PORT", "6379")),
password=os.getenv("REDIS_PASSWORD") or None,
db=int(os.getenv("REDIS_DB", "0")),
decode_responses=True,
max_connections=int(os.getenv("REDIS_POOL_MAX", "50")),
socket_timeout=5,
socket_connect_timeout=2,
health_check_interval=30,
retry_on_timeout=True,
)
return redis.Redis(connection_pool=pool)
| 参数 | 建议 |
|---|---|
max_connections |
API:按 QPS×RTT 估算;Worker:并发协程数 + 余量 |
| 进程模型 | 每进程一个 get_redis();不要全局跨 fork 共享池(Celery prefork 需在子进程重建) |
| TLS | ConnectionPool(..., connection_class=redis.SSLConnection, ssl_cert_reqs="required") |
异步(FastAPI / asyncio Agent):用独立池,勿与同步混用。
python
import redis.asyncio as aioredis
@lru_cache
def get_redis_async() -> aioredis.Redis:
return aioredis.from_url(
os.getenv("REDIS_URL", "redis://127.0.0.1:6379/0"),
max_connections=int(os.getenv("REDIS_POOL_MAX", "50")),
decode_responses=True,
socket_timeout=5,
)
# async def handler():
# r = get_redis_async()
# await r.xread({stream: last_id}, block=5000)
Pipeline 与池 :r.pipeline() 仍走同一客户端的池;高吞吐写 checkpoint 时 batch 可减少 RTT(§10c)。
1.3 Sentinel(主从 + 自动故障转移)
一主多从 + Sentinel 选举时,用 Sentinel + master_for,底层同样是连接池。
python
@lru_cache
def get_redis_sentinel() -> redis.Redis:
sentinel = redis.Sentinel(
[
(os.getenv("SENTINEL_HOST", "127.0.0.1"), int(os.getenv("SENTINEL_PORT", "26379"))),
],
socket_timeout=2,
password=os.getenv("SENTINEL_PASSWORD") or None,
)
# 服务名与 sentinel monitor 配置一致,如 "mymaster"
return sentinel.master_for(
os.getenv("REDIS_SENTINEL_MASTER", "mymaster"),
socket_timeout=5,
password=os.getenv("REDIS_PASSWORD") or None,
db=int(os.getenv("REDIS_DB", "0")),
decode_responses=True,
)
# 只读副本(会话读、缓存读)可路由到 slave_for,注意复制延迟
def get_redis_replica() -> redis.Redis:
sentinel = redis.Sentinel([(os.getenv("SENTINEL_HOST", "127.0.0.1"), 26379)])
return sentinel.slave_for(
os.getenv("REDIS_SENTINEL_MASTER", "mymaster"),
decode_responses=True,
)
Agent 场景 :XREADGROUP 消费、分布式锁、幂等 必须写主;SSE 读历史/trace 在延迟敏感低时可读从库。
1.4 Redis Cluster(分片 + 水平扩展)
数据分片到多 master 时,使用 RedisCluster (redis>=4.3)。客户端根据 CRC16 slot 路由命令。
python
@lru_cache
def get_redis_cluster() -> RedisCluster:
# 方式 B(多节点 URL):return RedisCluster.from_url(os.environ["REDIS_CLUSTER_URL"], ...)
# 方式 A:至少配置一个种子节点,客户端会自动发现拓扑
return RedisCluster(
host=os.getenv("REDIS_CLUSTER_HOST", "127.0.0.1"),
port=int(os.getenv("REDIS_CLUSTER_PORT", "6379")),
password=os.getenv("REDIS_PASSWORD") or None,
decode_responses=True,
socket_timeout=5,
require_full_coverage=False, # slot 迁移期可放宽;稳定后配合监控
)
def tenant_key(tenant: str, *parts: str) -> str:
"""Hash Tag:花括号内子串决定 slot,保证同租户多 key 可 pipeline / 事务。"""
return ":".join([f"{{{tenant}}}", "harness", *parts])
# 示例:锁 + checkpoint 同 slot
# lock: {acme}:harness:lock:repo:main
# session:{acme}:harness:session:sess_01
| Cluster 限制 | 对 Agent 的影响 |
|---|---|
| 多 key 命令须同 slot | MGET、事务、Lua 多 key → 用 同一 {tag} |
KEYS 禁用 |
用 SCAN + 前缀;按 tenant 设计 key |
| Pub/Sub | channel 不跨 slot 路由;大规模广播慎用,优先 Stream |
XREAD 多 stream |
多个 stream 可能在不同节点;多 stream 的 XREAD 要求所有 key 同 slot → 多 run 并行 tail 时用 每 run 独立连接轮询 ,或 每 run 一个带 tag 的 stream 名 且接受限制 |
db 编号 |
Cluster 仅 db 0 |
Stream 在 Cluster 上 :单个 Stream key 落在一个 slot,XADD/XREAD/XREADGROUP 与单机一致;不要 对多个不同 run 的 stream 做 一条 XREAD 跨 slot(§6.4 多 run 监听在 Cluster 下需改为循环单 stream 或聚合服务)。
python
# Cluster 下推荐:单 run 单 stream(天然单 key)
def xread_one_run_cluster(run_id: str, last_id: str) -> list:
stream = tenant_key("acme", "events", "run", run_id) # 全 key 同一 {acme}
rc = get_redis_cluster()
return rc.xread({stream: last_id}, block=5000, count=50) or []
1.5 统一工厂(按环境变量切换)
python
@lru_cache
def get_redis() -> redis.Redis | RedisCluster:
mode = os.getenv("REDIS_MODE", "pool") # single | pool | sentinel | cluster
if mode == "single":
return redis_single()
if mode == "sentinel":
return get_redis_sentinel()
if mode == "cluster":
return get_redis_cluster()
return get_redis_pool()
# 后文业务示例统一:
r = get_redis()
REDIS_MODE |
典型部署 |
|---|---|
pool |
单机 Redis / 云厂商单实例 |
sentinel |
自建主从 + Sentinel |
cluster |
大数据量、高写 QPS、需要分片 |
1.6 部署形态对照(选型)
| 形态 | 连接方式 | 扩展 | Agent 常用 |
|---|---|---|---|
| 单机 | ConnectionPool |
垂直扩容 | 开发、中小规模 |
| 主从 + Sentinel | Sentinel.master_for |
读扩展、故障转移 | 生产主流 |
| Redis Cluster | RedisCluster |
水平分片 | 海量 session / 多 Stream |
| 托管(ElastiCache 等) | 厂商 URL + TLS | 按产品选 replica/cluster | 少运维 |
小结 :正文示例逻辑不变,只把 r = redis.Redis(...) 换成 r = get_redis() ;上 Cluster 时 key 带 {tenant} tag ,并避免跨 slot 多 key XREAD。
2. 会话态与 Harness Checkpoint(Hash + TTL)
长跑 Agent 跨多个 context window 时,Harness 需要 结构化交接物(进度、plan、最近工具输出摘要)。Redis Hash 适合「字段级更新」,避免每次全量覆盖一个大 JSON。
python
SESSION_TTL = 60 * 60 * 24 # 24h
def save_checkpoint(session_id: str, patch: dict[str, Any]) -> None:
k = key("harness", "session", session_id)
pipe = r.pipeline()
for field, value in patch.items():
pipe.hset(k, field, json.dumps(value, ensure_ascii=False))
pipe.expire(k, SESSION_TTL)
pipe.execute()
def load_checkpoint(session_id: str) -> dict[str, Any]:
k = key("harness", "session", session_id)
raw = r.hgetall(k)
return {f: json.loads(v) for f, v in raw.items()}
# 使用示例
sid = "sess_" + uuid.uuid4().hex[:12]
save_checkpoint(sid, {
"phase": "coding",
"features_done": ["auth", "billing"],
"last_error": None,
})
state = load_checkpoint(sid)
Harness 要点 :Checkpoint 字段应由 Orchestrator 写入,Coding Agent 只读/增量写;避免把整段对话原文塞进 Redis(放对象存储 + 只存摘要与指针)。
3. 分布式锁:同一资源禁止并发工具执行(SET NX + Lua 安全释放)
例如:两个 Agent 实例同时改同一 Git 分支、同一 K8s Deployment。锁必须 带 token,释放时校验 token,防止误删他人锁。
python
LOCK_TTL_MS = 30_000
def acquire_lock(resource: str, owner: str) -> str | None:
token = f"{owner}:{uuid.uuid4().hex}"
ok = r.set(
key("harness", "lock", resource),
token,
nx=True,
px=LOCK_TTL_MS,
)
return token if ok else None
_RELEASE_LUA = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
"""
def release_lock(resource: str, token: str) -> bool:
deleted = r.eval(
_RELEASE_LUA,
1,
key("harness", "lock", resource),
token,
)
return bool(deleted)
# 使用示例
resource = "repo:HarnessAgent:branch:main"
token = acquire_lock(resource, owner="worker-1")
if not token:
raise RuntimeError("resource busy")
try:
... # 执行 git pull / apply patch
finally:
release_lock(resource, token)
生产提示 :长任务用 看门狗续期 (后台线程定期 PEXPIRE);更复杂场景可用 Redlock 或 etcd,但单 Redis 实例 + 合理 TTL 已覆盖多数 Agent 工具链。
4. 幂等键:防止重试导致重复扣费、重复发邮件(SET NX)
HTTP/Webhook 触发 Agent 时,客户端常会重试。Harness 在入口用 idempotency key 保证「同一业务请求只执行一次」。
python
IDEM_TTL = 60 * 60 # 1h
def begin_idempotent(run_id: str) -> bool:
"""返回 True 表示首次执行;False 表示重复请求应直接返回缓存结果。"""
return bool(
r.set(
key("harness", "idem", run_id),
"processing",
nx=True,
ex=IDEM_TTL,
)
)
def finish_idempotent(run_id: str, result: dict[str, Any]) -> None:
k = key("harness", "idem", run_id)
r.set(k, json.dumps(result), ex=IDEM_TTL)
def get_idempotent_result(run_id: str) -> dict[str, Any] | None:
v = r.get(key("harness", "idem", run_id))
if not v or v == "processing":
return None
return json.loads(v)
5. Token / 成本预算与全局限流(INCR + 固定窗口 / 滑动窗口)
5.1 按租户日预算(简单固定窗口)
python
def consume_token_budget(tenant: str, tokens: int, daily_limit: int) -> bool:
k = key("harness", "budget", tenant, time.strftime("%Y%m%d"))
used = r.incrby(k, tokens)
if used == tokens:
r.expire(k, 60 * 60 * 48)
return used <= daily_limit
5.2 滑动窗口限流(ZSET,适合 API QPS)
python
def allow_request(user_id: str, limit: int, window_sec: int) -> bool:
now = time.time()
k = key("harness", "rl", user_id)
pipe = r.pipeline()
pipe.zremrangebyscore(k, 0, now - window_sec)
pipe.zadd(k, {str(uuid.uuid4()): now})
pipe.zcard(k)
pipe.expire(k, window_sec + 1)
_, _, count, _ = pipe.execute()
return count <= limit
Harness 要点 :限流应在 调用 LLM / 昂贵工具之前 由策略层执行,失败时返回结构化错误,便于 Agent 改计划而非盲目重试。
6. Stream 深度:XADD / XREAD / XREADGROUP(Agent 事件总线核心)
智能体系统里 Stream 比 List 多两样能力:每条消息有 ID(可回溯) 、消费者组( competing worker + ACK) 。
很多人只写了 XREADGROUP,却漏了 XREAD ------它才是 「多个订阅方各自读全量事件」(UI 实时日志、审计、旁路分析)的标准姿势。
6.0 命令速查:什么时候用谁
| 命令 | 典型场景 | 是否竞争消费 | 是否 ACK | Agent 例子 |
|---|---|---|---|---|
XADD |
写入事件 | --- | --- | tool_start / tool_end / token_delta |
XREAD |
Fan-out:每个读者独立游标 | 否(各自 last_id) | 无 | 前端 SSE tail run;审计落库 |
XREAD + BLOCK |
阻塞等新事件 | 否 | 无 | Orchestrator 盯「人类已批准」事件流 |
XREADGROUP |
任务队列:一条只给一个 Worker | 是 | 要 XACK |
多实例跑 Coding Agent |
XPENDING / XCLAIM |
僵尸 pending 重领 | 是 | 重领后仍要 ACK | Worker 崩溃后任务不丢 |
XRANGE |
按 ID 区间重放 | --- | --- | 复盘某次 run 的全链路 |
XTRIM / MAXLEN |
控制内存 | --- | --- | Trace 只保留最近 1 万条 |
XADD
XREAD 各自 last_id
XREAD 各自 last_id
XREADGROUP 竞争
XREADGROUP 竞争
Producer
Harness / Tool Runtime
Stream
UI / SSE
Audit / Metrics
Worker-1
Worker-2
6.1 List:轻量单消费者队列(对比用)
无消息 ID、无回溯、多消费者会抢同一条,只适合 极简后台任务。
python
QUEUE = key("harness", "queue", "tool-jobs")
def enqueue_job(payload: dict[str, Any]) -> None:
r.lpush(QUEUE, json.dumps(payload))
def dequeue_job(block_sec: int = 5) -> dict[str, Any] | None:
item = r.brpop(QUEUE, timeout=block_sec)
if not item:
return None
_, raw = item
return json.loads(raw)
6.2 XADD:统一事件信封(建议字段)
python
EVENTS = key("harness", "events", "run") # 实际: events:run:{run_id}
def emit(run_id: str, kind: str, data: dict[str, Any]) -> str:
"""返回 Redis Stream ID,如 1715000000000-0"""
stream = f"{EVENTS}:{run_id}"
return r.xadd(
stream,
{
"kind": kind, # tool_start | tool_end | llm_delta | error | done
"data": json.dumps(data, ensure_ascii=False),
"ts": str(time.time()),
},
)
# CLI 等价:
# XADD harness:events:run:sess_abc * kind tool_start data '{"tool":"bash"}'
6.3 XREAD:Fan-out 实时 tail(无消费者组)
要点 :每个读者维护自己的 last_id。从开头读用 0-0;只读新消息用 $(仅对「第一次 BLOCK 之后」有效,见 6.4)。
python
def xread_once(stream: str, last_id: str = "0-0", count: int = 100) -> list[tuple[str, dict]]:
"""非阻塞:拉一批历史或增量。"""
rows = r.xread({stream: last_id}, count=count)
if not rows:
return []
out = []
for _name, entries in rows:
for msg_id, fields in entries:
out.append((msg_id, fields))
return out
def xread_block_loop(stream: str, last_id: str = "$", block_ms: int = 15_000):
"""
阻塞 tail:Agent UI / 旁路监控的标准写法。
last_id='$' 表示:先建立连接,之后只收新 XADD。
"""
cursor = last_id
while True:
rows = r.xread({stream: cursor}, block=block_ms, count=50)
if not rows:
continue
for _name, entries in rows:
for msg_id, fields in entries:
cursor = msg_id # 关键:推进游标,避免重复
yield msg_id, fields
Harness 场景:SSE 把 Agent 步进推给浏览器
python
# FastAPI 风格伪代码
async def sse_run_events(run_id: str):
stream = f"{EVENTS}:{run_id}"
last_id = "0-0" # 新连接可从开头回放;只要 live 可改为 $
for msg_id, fields in xread_block_loop(stream, last_id=last_id):
payload = {"id": msg_id, "kind": fields["kind"], "data": json.loads(fields["data"])}
yield f"data: {json.dumps(payload)}\n\n"
if fields["kind"] == "done":
break
redis-cli 示例(可先 XADD 再 XREAD)
bash
XADD harness:events:run:demo * kind tool_start data '{"tool":"grep"}'
XREAD COUNT 10 STREAMS harness:events:run:demo 0-0
# 阻塞 5s 等新消息(另开终端 XADD 可看到返回)
XREAD BLOCK 5000 STREAMS harness:events:run:demo $
6.4 XREAD:多 Stream、多 run 同时盯(Orchestrator)
一个调度器同时 watch 多个 run 的审批/完成事件:
python
def xread_many(streams: dict[str, str], block_ms: int = 5000) -> list[tuple[str, str, dict]]:
"""
streams = { stream_key: last_id, ... }
返回 (stream_name, msg_id, fields)
"""
rows = r.xread(streams, block=block_ms, count=20)
result = []
for stream_name, entries in rows:
for msg_id, fields in entries:
result.append((stream_name, msg_id, fields))
return result
# 使用:同时监听 3 个 run 的 done 事件
cursors = {
f"{EVENTS}:run_a": "$",
f"{EVENTS}:run_b": "$",
f"{EVENTS}:run_c": "$",
}
while True:
batch = xread_many(cursors, block_ms=10_000)
for stream_name, msg_id, fields in batch:
cursors[stream_name] = msg_id
if fields["kind"] == "done":
... # 回收资源 / 更新 DB
6.5 XREAD vs XREADGROUP:选型口诀
| 需求 | 用 |
|---|---|
| 每条消息要被 UI、审计、指标 各读一遍 | XREAD(各自 last_id) |
| 每条任务只能被 一个 Worker 执行 | XREADGROUP + XACK |
| 读者挂了不能丢、要 pending 重领 | XREADGROUP + XPENDING / XCLAIM |
| 极简、不在乎 ID | BRPOP List |
6.6 XREADGROUP:竞争消费 + ACK(Orchestrator 任务队列)
python
RUN_STREAM = key("harness", "stream", "runs")
GROUP = "orchestrator-workers"
def ensure_group(stream: str = RUN_STREAM, start_id: str = "0") -> None:
try:
r.xgroup_create(stream, GROUP, id=start_id, mkstream=True)
except redis.ResponseError as e:
if "BUSYGROUP" not in str(e):
raise
def publish_run(run: dict[str, Any]) -> str:
return r.xadd(RUN_STREAM, {"payload": json.dumps(run)})
def consume_runs_group(consumer: str, count: int = 1, block_ms: int = 5000):
"""
'>' = 只拉从未投递给本组的新消息。
处理成功后必须 xack,否则进入 pending。
"""
ensure_group()
rows = r.xreadgroup(
groupname=GROUP,
consumername=consumer,
streams={RUN_STREAM: ">"},
count=count,
block=block_ms,
)
for _stream, entries in rows or []:
for msg_id, fields in entries:
yield msg_id, json.loads(fields["payload"])
def ack_run(msg_id: str) -> None:
r.xack(RUN_STREAM, GROUP, msg_id)
# Worker 主循环
def worker_loop(consumer: str) -> None:
for msg_id, run in consume_runs_group(consumer):
try:
execute_agent_run(run) # 你的 Harness 入口
ack_run(msg_id)
except Exception:
# 不 ACK → 留在 pending,由 XPENDING/XCLAIM 处理
raise
redis-cli
bash
XGROUP CREATE harness:stream:runs orchestrator-workers 0 MKSTREAM
XADD harness:stream:runs * payload '{"run_id":"r1"}'
XREADGROUP GROUP orchestrator-workers w1 COUNT 1 BLOCK 5000 STREAMS harness:stream:runs >
XACK harness:stream:runs orchestrator-workers <msg-id>
6.7 启动时先清 pending:0 与 > 的区别
| Stream 参数 | 含义 |
|---|---|
> |
只要 新进入组 的消息(常规主循环) |
0 |
从 pending 列表 里拿本 consumer 未 ACK 的历史(重启恢复) |
python
def consume_pending_self(consumer: str, count: int = 10):
"""进程重启后,先处理自己名下的 pending,再切 '>'."""
rows = r.xreadgroup(GROUP, consumer, {RUN_STREAM: "0"}, count=count)
for _s, entries in rows or []:
for msg_id, fields in entries:
yield msg_id, json.loads(fields["payload"])
6.8 XPENDING / XCLAIM:Worker 崩溃不丢任务
python
def pending_summary() -> list[dict]:
"""查看组内未 ACK 消息(idle 过久可被别的 worker 领走)。"""
# redis-py: xpending_range 封装
pending = r.xpending_range(RUN_STREAM, GROUP, min="-", max="+", count=20)
return [
{
"id": p["message_id"],
"consumer": p["consumer"],
"idle_ms": p["time_since_delivered"],
"delivery_count": p["times_delivered"],
}
for p in pending
]
def claim_stale(min_idle_ms: int = 60_000, new_consumer: str = "worker-recovery") -> list[str]:
"""idle 超过阈值的 pending 划归新 consumer(Agent 节点宕机场景)。"""
pending = r.xpending_range(RUN_STREAM, GROUP, min="-", max="+", count=50)
claimed_ids = []
for p in pending:
if p["time_since_delivered"] < min_idle_ms:
continue
msgs = r.xclaim(
RUN_STREAM,
GROUP,
new_consumer,
min_idle_time=min_idle_ms,
message_ids=[p["message_id"]],
)
for msg_id, fields in msgs:
claimed_ids.append(msg_id)
retry_run(json.loads(fields["payload"]))
r.xack(RUN_STREAM, GROUP, msg_id)
return claimed_ids
Redis 7+ 可用 XAUTOCLAIM 一次拉一批 idle 消息(语义同多次 XCLAIM)。
6.9 XRANGE / XREVRANGE:按 ID 重放与调试
python
def replay_run(run_id: str, start: str = "-", end: str = "+", count: int = 500) -> list[dict]:
stream = f"{EVENTS}:{run_id}"
entries = r.xrange(stream, min=start, max=end, count=count)
events = []
for msg_id, fields in entries:
events.append({
"id": msg_id,
"kind": fields["kind"],
"data": json.loads(fields["data"]),
"ts": float(fields["ts"]),
})
return events
def last_n_events(run_id: str, n: int = 20) -> list[dict]:
stream = f"{EVENTS}:{run_id}"
entries = r.xrevrange(stream, max="+", min="-", count=n)
return list(reversed([
{"id": mid, "kind": f["kind"]} for mid, f in entries
]))
用途 :用户投诉「Agent 为什么删了文件」→ 用 XRANGE 还原 tool_start/tool_end 时间线,无需翻模型对话。
6.10 XTRIM / XADD MAXLEN:Trace 防爆内存
python
def append_trace_capped(run_id: str, event: dict[str, Any], maxlen: int = 10_000) -> str:
stream = f"{EVENTS}:{run_id}"
# approximate=True 性能更好,长度略有浮动
return r.xadd(stream, {"e": json.dumps(event)}, maxlen=maxlen, approximate=True)
def trim_old(stream: str, maxlen: int = 50_000) -> int:
return r.xtrim(stream, maxlen=maxlen, approximate=True)
6.11 完整对照:同一条 run 的三类读者
审计 XREAD last_id Worker XREADGROUP 前端 SSE XREAD last_id Stream events:run:1 Harness 审计 XREAD last_id Worker XREADGROUP 前端 SSE XREAD last_id Stream events:run:1 Harness XADD tool_start XREAD BLOCK 推送 XREAD BLOCK 写 ClickHouse XADD job_step XREADGROUP 投递 执行工具 XADD tool_end XACK
python
# 三行总结
# UI / 审计: r.xread({stream: last_id}, block=15000)
# 任务 Worker: r.xreadgroup(GROUP, consumer, {stream: ">"}, ...)
# 历史复盘: r.xrange(stream, "-", "+")
7. Pub/Sub:多 Agent 实时协调(注意:无持久化)
适合 进度广播、取消信号;不适合「至少一次」任务投递(用 Stream)。
python
CHANNEL_CANCEL = key("harness", "channel", "cancel")
def publish_cancel(run_id: str) -> None:
r.publish(CHANNEL_CANCEL, run_id)
def subscribe_cancel():
pubsub = r.pubsub()
pubsub.subscribe(CHANNEL_CANCEL)
for msg in pubsub.listen():
if msg["type"] == "message":
yield msg["data"] # run_id
Worker 在长跑循环中轮询取消:
python
def is_cancelled(run_id: str) -> bool:
return r.sismember(key("harness", "cancelled"), run_id)
def mark_cancelled(run_id: str) -> None:
r.sadd(key("harness", "cancelled"), run_id)
r.expire(key("harness", "cancelled"), 3600)
publish_cancel(run_id)
8. LLM 响应缓存与语义缓存(String + TTL)
8.1 精确缓存(相同 model + messages hash)
python
import hashlib
def cache_key(model: str, messages: list[dict]) -> str:
blob = json.dumps({"model": model, "messages": messages}, sort_keys=True)
h = hashlib.sha256(blob.encode()).hexdigest()
return key("harness", "llm-cache", h)
def get_cached_llm(model: str, messages: list[dict]) -> str | None:
return r.get(cache_key(model, messages))
def set_cached_llm(model: str, messages: list[dict], text: str, ttl: int = 3600) -> None:
r.setex(cache_key(model, messages), ttl, text)
8.2 语义缓存(思路)
- 对 user query 算 embedding;
- 在 Redis Vector(Redis Stack)或外部向量库做 近似最近邻;
- 距离低于阈值则直接返回历史答案。
Harness 层应记录 cache_hit 指标,并允许按租户关闭缓存(合规场景)。
9. 人机回路(HITL):审批队列与超时(List + ZSET)
python
APPROVAL_QUEUE = key("harness", "approval", "pending")
APPROVAL_DEADLINE = key("harness", "approval", "deadline")
def request_approval(run_id: str, summary: str, timeout_sec: int = 1800) -> None:
payload = json.dumps({"run_id": run_id, "summary": summary})
r.lpush(APPROVAL_QUEUE, payload)
r.zadd(APPROVAL_DEADLINE, {run_id: time.time() + timeout_sec})
def pop_approval() -> dict | None:
item = r.brpop(APPROVAL_QUEUE, timeout=1)
if not item:
return None
return json.loads(item[1])
def approve(run_id: str) -> None:
r.setex(key("harness", "approval", "ok", run_id), 3600, "1")
r.zrem(APPROVAL_DEADLINE, run_id)
def is_approved(run_id: str) -> bool:
return r.exists(key("harness", "approval", "ok", run_id)) == 1
def expired_run_ids(now: float | None = None) -> list[str]:
now = now or time.time()
return r.zrangebyscore(APPROVAL_DEADLINE, 0, now)
Agent 在执行危险工具前阻塞等待 is_approved 或超时策略(自动拒绝/降级)。
10. 工具 Trace:XADD 写 + XREAD 推(与 §6 同一套流)
调试与可观测的标准路径:Harness 用 XADD 写步进;前端/CLI 用 XREAD BLOCK tail;复盘用 XRANGE(见 §6.9、§6.10)。不要只用 Pub/Sub------断线期间事件会丢。
python
TRACE_PREFIX = key("harness", "trace", "run")
def trace_stream(run_id: str) -> str:
return f"{TRACE_PREFIX}:{run_id}"
def append_trace(run_id: str, event: dict[str, Any], maxlen: int = 10_000) -> str:
return r.xadd(
trace_stream(run_id),
{"e": json.dumps(event, ensure_ascii=False)},
maxlen=maxlen,
approximate=True,
)
def tail_trace_cli(run_id: str) -> None:
"""本地调试:打印 live trace。"""
stream = trace_stream(run_id)
last_id = "0-0"
while True:
rows = r.xread({stream: last_id}, block=5000, count=100)
if not rows:
continue
for _n, entries in rows:
for msg_id, fields in entries:
last_id = msg_id
print(msg_id, json.loads(fields["e"]))
10b. 延迟重试:ZSET 调度 + Stream 触发(Agent 工具失败退避)
工具调用失败需要 指数退避 时:ZSET 存「何时可重试」,调度器 ZRANGEBYSCORE 到期后 XADD 到任务 Stream,Worker 仍用 XREADGROUP 消费。
python
RETRY_ZSET = key("harness", "retry", "at")
RETRY_STREAM = key("harness", "stream", "retries")
def schedule_retry(run_id: str, payload: dict, execute_at: float) -> None:
r.zadd(RETRY_ZSET, {json.dumps({"run_id": run_id, **payload}): execute_at})
def flush_due_retries(now: float | None = None) -> int:
now = now or time.time()
due = r.zrangebyscore(RETRY_ZSET, 0, now, start=0, num=100)
if not due:
return 0
pipe = r.pipeline()
for item in due:
pipe.xadd(RETRY_STREAM, {"payload": item})
pipe.zrem(RETRY_ZSET, item)
pipe.execute()
return len(due)
# 调度循环(独立进程)
# while True: flush_due_retries(); sleep(1)
# Worker: xreadgroup 消费 RETRY_STREAM
10c. Pipeline:批量写 Checkpoint(降 RTT)
Agent 一步可能更新多个 Hash 字段 + 发多条事件,用 pipeline 合并往返:
python
def save_step_bundle(session_id: str, checkpoint: dict, events: list[dict]) -> None:
sk = key("harness", "session", session_id)
ek = f"{EVENTS}:{session_id}"
pipe = r.pipeline()
for field, value in checkpoint.items():
pipe.hset(sk, field, json.dumps(value))
pipe.expire(sk, SESSION_TTL)
for ev in events:
pipe.xadd(ek, {"kind": ev["kind"], "data": json.dumps(ev["data"])})
pipe.execute()
11. 多 Agent 分工:租约与心跳(String + TTL)
Initializer / Coding / Reviewer 多角色时,用 租约 标记当前谁「持有」该 run:
python
def claim_run(run_id: str, role: str, ttl_sec: int = 120) -> bool:
return bool(
r.set(
key("harness", "lease", run_id),
role,
nx=True,
ex=ttl_sec,
)
)
def heartbeat(run_id: str, role: str, ttl_sec: int = 120) -> None:
# 仅当仍是同一 role 时续期(Lua 更安全,此处略)
current = r.get(key("harness", "lease", run_id))
if current == role:
r.expire(key("harness", "lease", run_id), ttl_sec)
12. 特性开关与熔断状态(Hash / String)
python
FLAGS = key("harness", "flags")
def flag_enabled(name: str, default: bool = False) -> bool:
v = r.hget(FLAGS, name)
if v is None:
return default
return v == "1"
def record_tool_failure(tool: str) -> int:
k = key("harness", "cb", tool)
n = r.incr(k)
r.expire(k, 300)
return n
def circuit_open(tool: str, threshold: int = 5) -> bool:
return record_tool_failure(tool) >= threshold # 示例:应用层读次数后拒绝
17. 前后端:会话、SSE 桥、上传凭证、并发 Run 上限
智能体控制台常见链路:前端 → API → Redis → Worker 。Redis 存 会话与 run 元数据 ;步进流 用 Stream,API 层 XREAD 转 SSE(§6.3)。
17.1 服务端会话(替代把 state 全塞 JWT)
python
def create_user_session(user_id: str, meta: dict) -> str:
sid = "usess_" + uuid.uuid4().hex[:16]
k = key("harness", "web", "session", sid)
r.hset(k, mapping={
"user_id": user_id,
"meta": json.dumps(meta),
})
r.expire(k, 60 * 60 * 8)
return sid
def bind_run_to_session(session_id: str, run_id: str) -> None:
r.sadd(key("harness", "web", "session", session_id, "runs"), run_id)
r.expire(key("harness", "web", "session", session_id, "runs"), 60 * 60 * 8)
17.2 BFF:Redis Stream → SSE(前端消费端)
python
# GET /api/runs/{run_id}/events (FastAPI/Flask 伪代码)
def sse_from_redis(run_id: str, after_id: str | None = None):
stream = f"{EVENTS}:{run_id}"
last_id = after_id or "0-0"
while True:
batches = r.xread({stream: last_id}, block=20_000, count=200)
if not batches:
yield ": keepalive\n\n"
continue
for _n, entries in batches:
for msg_id, fields in entries:
last_id = msg_id
yield f"id: {msg_id}\ndata: {json.dumps(fields)}\n\n"
if fields.get("kind") == "done":
return
前端用 EventSource;断线重连带 Last-Event-ID = Redis msg id,即可续传。
17.3 短期上传凭证(工具产物、截图进沙箱)
python
def issue_upload_token(run_id: str, ttl: int = 600) -> str:
token = uuid.uuid4().hex
r.setex(key("harness", "upload", token), ttl, run_id)
return token
def verify_upload_token(token: str) -> str | None:
return r.get(key("harness", "upload", token))
17.4 每用户并发 Run 上限(前后端都拦)
python
def try_start_run(user_id: str, run_id: str, max_concurrent: int = 3) -> bool:
k = key("harness", "user", user_id, "active_runs")
n = r.scard(k)
if n >= max_concurrent:
return False
r.sadd(k, run_id)
r.expire(k, 3600)
return True
def finish_run(user_id: str, run_id: str) -> None:
r.srem(key("harness", "user", user_id, "active_runs"), run_id)
18. 端云一体:边缘 Agent、离线队列、云端同步
端云一体 指:边缘设备/桌面端也跑轻量 Agent(采集、预处理),云端 跑重模型与工具;Redis 在云端做 汇聚与指令下发。
18.1 设备注册与心跳
python
def device_heartbeat(device_id: str, meta: dict) -> None:
now = time.time()
r.hset(key("harness", "device", device_id), mapping={
"last_seen": str(now),
"meta": json.dumps(meta),
})
r.zadd(key("harness", "devices", "online"), {device_id: now})
def stale_devices(older_than_sec: int = 120) -> list[str]:
cutoff = time.time() - older_than_sec
return r.zrangebyscore(key("harness", "devices", "online"), 0, cutoff)
18.2 云端 → 边缘:下行指令 Stream
边缘端通过 长轮询 API 或 MQTT 网关 间接读 Redis;云端直接 XADD:
python
DOWNLINK = key("harness", "downlink") # 实际: downlink:{device_id}
def push_command(device_id: str, cmd: dict) -> str:
stream = f"{DOWNLINK}:{device_id}"
return r.xadd(stream, {"cmd": json.dumps(cmd)})
# 边缘同步服务(云端侧代理)对单设备 XREAD
def fetch_commands(device_id: str, cursor: str) -> tuple[str, list[dict]]:
stream = f"{DOWNLINK}:{device_id}"
rows = r.xread({stream: cursor}, block=1000, count=50)
cmds, last = [], cursor
for _n, entries in rows or []:
for mid, fields in entries:
last = mid
cmds.append(json.loads(fields["cmd"]))
return last, cmds
18.3 边缘 → 云端:上行合并 + 版本冲突
python
def edge_push_checkpoint(device_id: str, version: int, patch: dict) -> bool:
"""CAS:仅当云端 version 小于边缘上报 version 时合并。"""
k = key("harness", "edge", "state", device_id)
current = int(r.hget(k, "version") or 0)
if version <= current:
return False # 云端更新,边缘应拉取
pipe = r.pipeline()
pipe.hset(k, "version", version)
for f, v in patch.items():
pipe.hset(k, f, json.dumps(v))
pipe.execute()
return True
Harness 要点 :大文件、日志正文 不要 进 Redis;只同步 指针 (S3 key)与 结构化进度。
19. 流式:LLM Token、工具 stdout、多端同时 tail
「流式」在 Agent 里分两层:
- 模型 Token 流(delta);
- Harness 事件流(tool_start、log_line、done)。
推荐:统一写入同一 Stream ,kind 区分;前端一条 SSE 订阅。
python
def emit_token_delta(run_id: str, delta: str, seq: int) -> str:
return emit(run_id, "llm_delta", {"text": delta, "seq": seq})
def emit_log_line(run_id: str, line: str) -> str:
return emit(run_id, "log_line", {"line": line})
# 可选:Pub/Sub 做「极低延迟、可丢」的 Token 旁路(仅调试)
TOKEN_CHAN = key("harness", "channel", "token")
def publish_token_ephemeral(run_id: str, delta: str) -> None:
r.publish(TOKEN_CHAN, json.dumps({"run_id": run_id, "delta": delta}))
| 方式 | 延迟 | 断线可续 | 适用 |
|---|---|---|---|
Stream + XREAD |
低 | ✓(msg id) | 生产 UI、审计 |
| Pub/Sub | 最低 | ✗ | 临时调试、同源多订阅 |
| 仅 HTTP chunked | 中 | ✗ | 无 Redis 时的兜底 |
Gateway 聚合示例:Worker XADD → API XREAD BLOCK → 客户端 SSE(§17.2)。
20. 定时任务:ZSET 时间轮、周期扫 pending、会话清理
除 §10b 延迟重试 外,智能体系统常见 三类定时:
| 类型 | 做法 | 示例 |
|---|---|---|
| 一次性延迟 | ZSET score=unix ts | 30s 后重试工具 |
| 周期扫描 | Cron 进程 + ZRANGEBYSCORE |
每 1min 清理过期 run |
| 租约超时 | Key TTL + 扫描 | Worker 死亡释放锁 |
python
SCHEDULE = key("harness", "schedule", "once")
CRON_TICK = key("harness", "cron", "last_tick")
def schedule_once(job_id: str, payload: dict, run_at: float) -> None:
r.zadd(SCHEDULE, {json.dumps({"id": job_id, **payload}): run_at})
def run_scheduler_tick() -> int:
now = time.time()
due = r.zrangebyscore(SCHEDULE, 0, now, start=0, num=200)
if not due:
return 0
for item in due:
data = json.loads(item)
publish_run(data) # 投入 §6.6 的 RUN_STREAM
r.zrem(SCHEDULE, item)
r.set(CRON_TICK, str(now))
return len(due)
# 周期:清理 7 天前已 done 的 trace stream(应用层按 run 列表扫)
def cleanup_trace_streams(run_ids: list[str]) -> None:
for rid in run_ids:
r.delete(f"{EVENTS}:{rid}")
生产环境可用 K8s CronJob / 云函数 调 run_scheduler_tick;Redis 只存 调度事实,不替代完整 Job 系统(复杂 DAG 用 Temporal 等)。
21. 发布 + 消费:Pub/Sub、Stream、List 怎么选
| 模式 | 命令 | 语义 | Agent 用法 |
|---|---|---|---|
| 广播 | PUBLISH / SUBSCRIBE |
在线订阅者收到;离线即丢 | 全局「停服开关」、取消 run |
| 持久日志 | XADD + XREAD |
可回放;多读者独立游标 | SSE、审计、复盘 |
| 任务消费 | XADD + XREADGROUP |
竞争 + ACK + pending | Worker 执行 run |
| 简单队列 | LPUSH + BRPOP |
单消费者为主 | 内部脚本任务 |
python
# 模式订阅:某租户下所有 run 取消
def publish_tenant_cancel(tenant: str, run_id: str) -> None:
r.publish(key("harness", "cancel", tenant), run_id)
def subscribe_tenant_cancel(tenant: str):
ch = key("harness", "cancel", tenant)
ps = r.pubsub()
ps.subscribe(ch)
for msg in ps.listen():
if msg["type"] == "message":
yield msg["data"]
注意 :Pub/Sub 在 Redis Cluster 下 channel 与节点绑定,跨 slot 无事务;高可靠 取消/审批 建议 Stream 或 DB 为准,Pub/Sub 只做加速通知。
22. 其他高频场景(速查 + 片段)
22.1 Leader 选举:单实例扫 XPENDING
python
LEADER = key("harness", "leader", "scheduler")
def acquire_leader(holder: str, ttl: int = 15) -> bool:
return bool(r.set(LEADER, holder, nx=True, ex=ttl))
def renew_leader(holder: str, ttl: int = 15) -> None:
if r.get(LEADER) == holder:
r.expire(LEADER, ttl)
# 仅 leader 调用 claim_stale() / run_scheduler_tick()
22.2 死信队列 DLQ
python
DLQ = key("harness", "stream", "dlq")
def move_to_dlq(original_id: str, payload: dict, error: str) -> None:
r.xadd(DLQ, {
"original_id": original_id,
"payload": json.dumps(payload),
"error": error[:2000],
})
22.3 全局 Run 号
python
def next_run_id() -> str:
n = r.incr(key("harness", "seq", "run"))
return f"run_{n}"
22.4 Webhook 去重(短窗口)
python
def webhook_seen(delivery_id: str, ttl: int = 86400) -> bool:
"""True=已处理过,应跳过。"""
return not r.set(key("harness", "webhook", delivery_id), "1", nx=True, ex=ttl)
22.5 多租户 Key(Cluster Hash Tag)
与 §1.4 相同,生产 Cluster 必须用 tag:
python
def tenant_key(tenant: str, *parts: str) -> str:
return ":".join([f"{{{tenant}}}", "harness", *parts])
22.6 Serverless Function 无状态
每次 invocation:从 Redis load_checkpoint → 执行一步 → save_checkpoint + emit;不要假设本机内存有上一轮状态。
22.7 指标:HyperLogLog 估算 UV / 工具种类
python
def track_tool_use(run_id: str, tool_name: str) -> None:
r.pfadd(key("harness", "hll", "tools", run_id), tool_name)
def approx_tool_count(run_id: str) -> int:
return r.pfcount(key("harness", "hll", "tools", run_id))
13. 与 Harness Engineering 的映射(面试/设计评审用)
| Harness 模块 | Redis 典型用法 | 场景章节 |
|---|---|---|
| 记忆 / 状态 | Session Hash、Checkpoint、租约 | §2、§17、§18 |
| 规划 / 调度 | XREADGROUP 队列;ZSET 定时/延迟 |
§6、§10b、§20 |
| 执行 / 工具 | 分布式锁、幂等、熔断 | §3、§4、§12 |
| 校验 / 反思 | LLM 缓存;XRANGE 重放 |
§8、§6.9 |
| 安全 / 权限 | 限流、预算、HITL、上传凭证 | §5、§9、§17 |
| 观测 | XREAD tail、Trace、XRANGE |
§6、§10、§19 |
| 前后端 / BFF | SSE 桥、并发 Run 上限 | §17 |
| 端云一体 | 设备心跳、上下行 Stream、CAS | §18 |
| 流式输出 | Token/log Stream;Pub/Sub 旁路 | §19 |
| 发布消费 | Pub/Sub 广播 vs Stream 持久 | §7、§21 |
Redis
Harness Engineering
策略 / 预算
调度
执行
校验
限流/预算
队列/Stream
锁/幂等
会话/缓存
14. 生产环境实战(连接池 · TLS · ACL · 监控 · Key 规范)
开篇「生产请配合连接池、TLS、ACL、监控与 Key 命名规范」的 落地展开。与 §1(连怎么接)、§16(和 Kafka/Temporal 分工)配合阅读。
Redis 生产
安全
Agent 服务
API / BFF
Workers
TLS in-transit
ACL 最小权限
连接池
Exporter + 告警
14.1 连接池(生产必做)
已有示例 :§1.2 同步池、§1.2 异步池、§1.5 get_redis() 工厂。
| 实践 | 说明 |
|---|---|
| 进程单例 | @lru_cache 或应用 lifespan 内创建 一个 Redis(connection_pool=...) |
| 禁止 per-request | 每个 HTTP 请求 Redis(host=...) → 连接风暴、TIME_WAIT 暴涨 |
| 多 worker | Gunicorn/Uvicorn workers=N → 每进程一个池 ;max_connections × workers 小于 Redis maxclients |
| Celery prefork | 子进程 fork 后重建 池(父进程池不可继承) |
| 阻塞命令 | XREAD BLOCK、BRPOP 会 占满连接 ;阻塞消费者用 独立池 或专用连接,与 API 池分离 |
| 优雅退出 | shutdown 时 pool.disconnect() |
容量粗算:
text
Redis maxclients 余量 > API_pool_max × API_workers
+ Worker_pool_max × Worker_replicas
+ 阻塞 tail 连接数
+ 20% 运维余量
python
# 阻塞消费(SSE 代理 / XREADGROUP)建议独立池,避免占光 API 池
@lru_cache
def get_redis_blocking() -> redis.Redis:
pool = redis.ConnectionPool.from_url(
os.environ["REDIS_URL"],
max_connections=int(os.getenv("REDIS_BLOCKING_POOL_MAX", "10")),
decode_responses=True,
socket_timeout=None, # 长 BLOCK 不宜过短 socket_timeout
)
return redis.Redis(connection_pool=pool)
重试 :启用 retry_on_timeout=True;业务层对 ConnectionError / TimeoutError 做指数退避;写操作 结合幂等键(§4)避免重试双写。
14.2 Key 命名规范(生产)
14.2.1 推荐格式
text
harness:{env}:{tenant}:{domain}:{entity}:{id}[:{field}]
│ │ │ │ │
prod acme session sess_01 (Hash field 另说)
| 段 | 规则 | 示例 |
|---|---|---|
harness |
固定产品前缀 | 多产品共集群时防冲突 |
{env} |
dev / staging / prod |
禁止 prod 与 dev 共 db 且无 env 段 |
{tenant} |
多租户必填;单租户可用 _ |
Cluster 用 {acme} Hash Tag 包住 tenant |
{domain} |
业务域 | session、stream、lock、rl、cache |
{entity} |
类型 | run、user、device |
{id} |
主键 | run_128、usess_abc |
python
def tenant_key(tenant: str, *parts: str) -> str:
"""Cluster:{tenant} 决定 slot。"""
return ":".join(["harness", ENV, f"{{{tenant}}}", *parts])
def key_no_tenant(*parts: str) -> str:
return ":".join(["harness", ENV, *parts])
# ✓ harness:prod:{acme}:session:sess_01
# ✓ harness:prod:{acme}:events:run:run_128
# ✗ session:sess_01 (无 env/租户,难治理)
# ✗ harness:prod:run:1 (易与其它 run 混淆)
14.2.2 TTL 与体积
| Key 类型 | 建议 TTL | 值大小 |
|---|---|---|
| Web 会话 | 8h--24h | < 10KB |
| Run checkpoint | 24h--7d(done 后主动删) | < 64KB;更大放 S3 |
| 分布式锁 | 30s--5min + 看门狗 | token 字符串 |
| 幂等 | 1h--24h | 结果摘要,非全文 |
| LLM 缓存 | 1h--24h | 注意 PII;可按租户关 |
| Trace Stream | MAXLEN ~ + 定期删 run |
单行 < 4KB |
14.2.3 禁止项
- 不要
KEYS harness:*在生产执行 → 用SCAN+ 前缀,或按 tenant 维护索引 Set。 - 不要 把 API Key、用户密码、完整 prompt 写入 Redis(合规与泄漏面)。
- 不要 无 TTL 的临时 key。
- 不要 用
db1/db2区分租户(Cluster 仅 db0;用 key 前缀)。 - 不要 超大 String(> 512KB 警惕;> 1MB 改对象存储)。
14.2.4 按实体速查表
| 实体 | Key 模式 | 结构 | TTL |
|---|---|---|---|
| 用户会话 | ...:web:session:{sid} |
Hash | 8h |
| Run checkpoint | ...:{t}:session:run:{rid} |
Hash | 7d |
| 任务队列 | ...:stream:runs |
Stream | 持久/MAXLEN |
| Run 事件 tail | ...:{t}:events:run:{rid} |
Stream | MAXLEN |
| 分布式锁 | ...:{t}:lock:{resource} |
String | 30s |
| 幂等 | ...:idem:{id} |
String | 1h |
| 限流 | ...:rl:{user} |
ZSET | 窗口+1 |
| 日预算 | ...:budget:{tenant}:{date} |
String | 48h |
| 上传凭证 | ...:upload:{token} |
String | 10min |
| 特性开关 | ...:flags |
Hash | 无或长 TTL |
14.3 TLS(传输加密)
生产 必须 加密公网或跨 VPC 流量。托管 Redis 通常提供 rediss://(Redis over TLS)。
14.3.1 URL 方式(推荐)
python
# rediss:// 默认 SSL;云厂商常带证书校验
REDIS_URL = "rediss://:password@redis.example.com:6379/0"
pool = redis.ConnectionPool.from_url(
REDIS_URL,
max_connections=50,
decode_responses=True,
ssl_cert_reqs="required", # 校验服务端证书
ssl_ca_certs=os.getenv("REDIS_CA_CERT"), # 自签或私有 CA 时指定
)
r = redis.Redis(connection_pool=pool)
14.3.2 显式 SSLConnection
python
pool = redis.ConnectionPool(
host="redis.example.com",
port=6380,
password=os.environ["REDIS_PASSWORD"],
connection_class=redis.SSLConnection,
ssl_cert_reqs="required",
ssl_ca_certs="/etc/ssl/certs/redis-ca.pem",
max_connections=50,
decode_responses=True,
)
| 项 | 建议 |
|---|---|
ssl_cert_reqs |
生产用 required ,勿 none |
| 证书轮换 | CA 放 Secret/文件,支持热更新池重建 |
| Cluster / Sentinel | 各节点域名须在证书 SAN 内,或用云厂商统一入口 |
| 性能 | TLS 有 CPU 开销;同 AZ 部署、连接池复用可降低握手次数 |
14.4 ACL(访问控制,最小权限)
Redis 6+ ACL :按角色拆分 API 只读+写会话 、Worker 消费 Stream 、运维只读 INFO ,禁用 FLUSHALL、CONFIG、DEBUG。
14.4.1 服务端配置示例(redis.conf / 托管控制台)
text
# 应用 API:会话、限流、读 trace;不写全局 stream 消费组
ACL SETUSER harness_api on >api_secret ~harness:prod:* +@read +@write -@dangerous +xadd +xread +hset +set +incr +expire
# Worker:队列消费、锁、checkpoint
ACL SETUSER harness_worker on >worker_secret ~harness:prod:* +@read +@write -@dangerous +xreadgroup +xack +xclaim +set +eval
# 只读监控/排障
ACL SETUSER harness_ro on >ro_secret ~harness:* +@read +info +slowlog|get
# 保留 default 仅本地 admin,生产禁用弱口令
具体
+/-命令集按你实际用到的命令微调;原则:能写 stream 的不给FLUSHDB。
14.4.2 客户端使用不同账号
python
def get_redis_api() -> redis.Redis:
return redis.Redis.from_url(
os.environ["REDIS_URL_API"], # redis://harness_api:...@host/0
ssl_cert_reqs="required",
max_connections=30,
)
def get_redis_worker() -> redis.Redis:
return redis.Redis.from_url(
os.environ["REDIS_URL_WORKER"],
ssl_cert_reqs="required",
max_connections=50,
)
| 实践 | 说明 |
|---|---|
| 密钥管理 | 密码放 K8s Secret / Vault,不进镜像与 Git |
| 命令面 | Agent 禁止 把 Redis 当 Shell;工具层白名单命令 |
| 网络 | 安全组仅允许应用子网访问 6379;不暴露公网 |
| 审计 | 开启慢日志;敏感操作走变更流程 |
14.5 监控与告警
14.5.1 采集方式
| 方式 | 用途 |
|---|---|
| redis_exporter → Prometheus | 生产标准:QPS、内存、连接、延迟 |
| 云监控 | ElastiCache / Tair 自带 CPU、连接、命中率 |
| 应用埋点 | 池等待时间、XREADGROUP lag、业务 cache hit |
INFO / SLOWLOG |
排障 |
14.5.2 Agent 系统必盯指标
| 指标 | 来源 | 告警建议 |
|---|---|---|
redis_memory_used_bytes |
exporter | > 75% maxmemory |
redis_connected_clients |
exporter | 接近 maxclients |
redis_commands_duration_seconds |
exporter / APM | P99 突增 |
blocked_clients |
INFO | > 0 持续(大量 BLOCK) |
| Stream lag | XINFO GROUPS → lag |
lag > 阈值(Worker 跟不上) |
| Stream pending | XPENDING |
持续增长(ACK 失败/僵死) |
| 键空间 | 自定义 | 某前缀 key 数量日环比异常 |
| 复制延迟 | master_repl_offset 差 |
读从库时 > 1s 警惕 |
python
def stream_group_lag(stream: str, group: str) -> dict:
"""运维脚本:拉消费组积压。"""
groups = r.xinfo_groups(stream)
for g in groups:
if g["name"] == group:
return {"lag": g["lag"], "pending": g["pending"], "consumers": g["consumers"]}
return {}
14.5.3 日志与追踪
- 应用日志:记录
run_id、tenant、Redis 命令失败(勿打完整 value)。 - OpenTelemetry:span 标注
db.system=redis、db.operation(如XREADGROUP)。 - 慢查询:
CONFIG SET slowlog-log-slower-than 10000(10ms)+ 定期SLOWLOG GET。
14.5.4 告警 Runbook(简)
| 告警 | 可能原因 | 动作 |
|---|---|---|
| 内存高 | Trace 无 MAXLEN、缓存无 TTL | 查大 key、MEMORY USAGE key |
| pending 涨 | Worker 宕机、处理抛错未 ACK | XPENDING → XCLAIM / 修 Worker |
| 连接数爆 | 池未复用、BLOCK 连接过多 | 查代码、拆阻塞池 |
| 主从切换 | Sentinel 故障转移 | 客户端应自动重连;检查 master_for |
14.6 持久化、备份与高可用
| 场景 | RDB/AOF | 说明 |
|---|---|---|
| 纯缓存、可重建会话 | 可关或仅 RDB | 丢了可接受 |
| Stream 任务队列 | AOF everysec 或托管自动备份 | 宕机少丢 pending |
| 审批 / 计费相关 key | Redis + DB 双写,以 DB 为准 | Redis 不是唯一真相 |
- maxmemory-policy :缓存类用
allkeys-lru或volatile-lru;队列 Stream 慎用激进淘汰,优先 MAXLEN + 业务删。 - 备份:托管快照;自建则 RDB + AOF + 定期恢复演练。
- HA:生产至少 Sentinel 或云托管多 AZ(§1.3)。
14.7 部署与环境隔离
| 项 | 建议 |
|---|---|
| 环境 | dev / staging / prod 独立实例 或独立 ACL + key 前缀 harness:{env}: |
| 配置 | REDIS_URL、REDIS_MODE、REDIS_POOL_MAX 走环境变量/Secret |
| 发布 | 变更 Stream 消费者组名、key 结构时需 兼容期(双写或版本域) |
| 压测 | 模拟 XREAD BLOCK 连接数 + Worker XREADGROUP 与线上一致 |
14.8 生产上线 Checklist(可打印)
| # | 项 | 完成 |
|---|---|---|
| 1 | 使用 ConnectionPool + 进程单例(§1.2、§14.1) | ☐ |
| 2 | 阻塞 tail / 消费者 独立小池 | ☐ |
| 3 | TLS (rediss:// 或 SSLConnection,校验证书) |
☐ |
| 4 | ACL 分 API / Worker / 只读;禁用危险命令 | ☐ |
| 5 | Key 符合 harness:{env}:... + 租户 + TTL(§14.2) |
☐ |
| 6 | Cluster 使用 Hash Tag ;无跨 slot 多 key XREAD |
☐ |
| 7 | Stream MAXLEN;Trace/事件不无限增长 | ☐ |
| 8 | redis_exporter + 告警(内存、连接、lag、pending) | ☐ |
| 9 | 密钥不进 Redis;不进仓库 | ☐ |
| 10 | 幂等 + 锁 token;重试策略已测 | ☐ |
| 11 | 故障演练:主从切换、Worker 全挂恢复 pending | ☐ |
| 12 | 与 PG/S3/Kafka 职责边界清晰(§16) | ☐ |
15. 本地快速体验(Docker)
bash
# 仅开发;无 TLS/ACL
docker run -d --name redis-harness -p 6379:6379 redis:7-alpine
pip install redis
# 可选:本地体验 ACL(redis.conf 挂载)
# docker run ... redis:7-alpine redis-server --aclfile /etc/redis/users.acl
生产配置请直接对照 §14 ,勿用裸 6379 暴露公网。
16. 竞品、可替代方案与组合协作(Agent / Harness 语境)
Redis 在智能体架构里通常只占 「热路径」一层 (毫秒级状态、协调、轻队列)。没有银弹:会话真相、长任务 DAG、审计海量日志、向量检索 往往要换专用系统或与 Redis 分工协作。
下文 §16.0 为速查表(与对外分享用的浓缩版一致);§16.1--16.8 为展开说明、链接与架构图。
16.0 速查版:替代 vs 协作(浓缩)
按能力
| 能力 | 可替代 Redis 的 | 更推荐与 Redis 一起用 |
|---|---|---|
| 会话 / Checkpoint | PostgreSQL、DynamoDB | Redis 热 + PG 权威 |
| 任务队列 | RabbitMQ、Kafka、NATS、SQS | Redis 抢 Run;Kafka 审计归档 |
| 实时推前端 | 轮询 DB(差) | Redis Stream → BFF → SSE |
| 定时 / 长跑编排 | Temporal、Celery、Inngest | Redis 短延迟;Temporal 人审等跨天 |
| 分布式锁 | etcd、Consul | 短锁 Redis 即可 |
| LLM 缓存 | 进程 LRU、GPTCache | Redis 跨实例共享 |
| 语义 / RAG | Qdrant、pgvector、Milvus | Redis 热点 + 向量库全量 |
| 大日志 / Trace | Kafka、ClickHouse | Redis live tail + Kafka 冷存 |
| 文件产物 | --- | S3/MinIO(Redis 只存指针) |
Redis 协议「平替」
| 类型 | 说明 |
|---|---|
| Valkey、KeyDB、Dragonfly | 兼容 Redis 客户端;偏性能 / 许可 / 成本 |
| Memcached | 仅 KV 缓存;无 Stream / 锁 / ZSET,不能完整替代 Harness 用法 |
| 云托管 ElastiCache / Tair 等 | 生产最常见 |
常见组合(按规模)
| 阶段 | 栈 |
|---|---|
| MVP | Redis + PostgreSQL |
| 成长 | Redis Sentinel + S3 + OpenTelemetry |
| 规模化 | Redis Cluster + Kafka + Temporal + PG |
| 重 RAG | Redis + pgvector/Qdrant + GPTCache |
Python 常一起出现的包
| 包 | 和 Redis 的关系 |
|---|---|
redis-py / redis.asyncio |
直连 |
| Celery / arq / rq / dramatiq | Redis 或 RabbitMQ 作 broker |
LangGraph checkpoint-redis / checkpoint-postgres |
图状态;与自建 Checkpoint 可二选一 |
| GPTCache | 语义缓存,底层可接 Redis + 向量库 |
| Socket.IO redis-adapter | 多实例 WebSocket,用 Pub/Sub |
一句话分工
- Redis :毫秒级协调、Run 队列、
XREAD推流、限流锁。 - PostgreSQL:真相、审批、计费。
- Kafka:不可丢、可回放的海量事件。
- Temporal:多步、等人、跨天的 Agent 工作流。
- 对象存储:工具输出 / 日志正文。
不必「全押 Redis」,也不必「全不用 Redis」------多数是 Redis 扛热路径,其它系统扛持久与编排。
16.1 按 Harness 能力对照:谁能替、谁该组合(展开)
| Agent / Harness 能力 | 本文 Redis 用法 | 可替代(竞品) | 更常组合(协作) |
|---|---|---|---|
| 会话 / Checkpoint | Hash + TTL | PostgreSQL、SQLite(边缘)、DynamoDB | Redis 热 + PG 冷 ;LangGraph PostgresSaver |
| 限流 / Token 预算 | INCR、ZSET | API 网关(Kong/Envoy)、漏桶在边缘 | Redis 全局限流 + 网关 per-IP |
| 分布式锁 / 幂等 | SET NX、Lua | etcd、Consul、ZooKeeper、DynamoDB 锁 | 短锁 Redis;跨服务编排用 Temporal |
| 轻量任务队列 | List、XREADGROUP |
RabbitMQ 、NATS JetStream、SQS、Kafka | Redis 抢单 + Kafka 审计;或 Celery broker |
| 实时步进 / SSE | Stream XREAD |
无替代时需 HTTP 轮询 DB;Socket 服务 | Redis → BFF → SSE;Socket.io Redis Adapter |
| 广播(可丢) | Pub/Sub | NATS、Kafka compact topic | 通知用 Pub/Sub;事实用 Stream/Kafka |
| 定时 / 延迟 | ZSET | Temporal timer、Celery ETA、Inngest、CronJob+DB | Redis 轻调度 + Temporal 长跑 |
| 长跑工作流 / DAG | (仅点到) | Temporal、Prefect、Airflow、Dagster | Agent Run 编排首选 Temporal;Redis 作侧车 |
| LLM 精确缓存 | String | 进程内 LRU、GPTCache、CDN | Redis 跨实例共享 |
| 语义 / RAG 缓存 | Redis Stack Vector | Qdrant 、Milvus、Pinecone、pgvector | Redis 热点 + 向量库全量 |
| Trace / 审计海量 | Stream( capped) | Kafka、Pulsar、Loki、ClickHouse | Redis 实时 tail + Kafka 持久 |
| 工具产物 / 日志正文 | 只存指针 | S3、MinIO、GCS | Redis 元数据 + 对象存储 |
| 可观测 | INCR、HLL | OpenTelemetry、Prometheus | Redis 暂存 span 批写后端 |
| 多实例 Leader | SET NX | K8s Lease、etcd election | 单集群扫 pending 用 Redis 即可 |
冷路径 审计/分析
温数据 秒~分钟
热路径 毫秒级
异步刷盘
事件归档
Redis / Valkey
PostgreSQL
对象存储 S3
Kafka / Pulsar
ClickHouse
Agent Harness
16.2 同类软件:Redis 的「平替」与分支
| 产品 | 关系 | 说明 |
|---|---|---|
| Valkey | Redis 协议兼容 fork | 开源替代;客户端常直接当 Redis 用 |
| KeyDB | 多线程 Redis 兼容 | 更高单实例 QPS;运维生态小于 Redis |
| Dragonfly | 现代多线程、兼容 RESP | 大内存、高吞吐场景评估 |
| Memcached | 部分重叠 | 仅 KV 缓存,无 Stream/锁/复杂结构;适合纯 cache |
| KeyDB / Garnet(.NET) | 微软开源兼容 | .NET 栈可评估 |
| 云托管 ElastiCache / Azure Cache / 阿里云 Tair | 托管 Redis 协议 | Agent 生产最常见;Tair 有增强模块 |
选型提示 :要 Stream、分布式锁、Hash、ZSET 的 Agent Harness,不要 用 Memcached 单换 Redis;要合规/成本可看 Valkey 或云托管。
16.3 消息与队列:和 Redis Stream 怎么分工
| 软件 | 强项 | 弱项 / 注意 | Agent 典型用法 |
|---|---|---|---|
| Redis Stream | 低延迟、和会话同集群、运维简单 | 持久与回放弱于 Kafka;Cluster 多 stream 受限 | Run 队列、Trace tail、延迟 ZSET |
| Kafka | 高吞吐、长期保留、回放 | 重、延迟高于 Redis | 全链路 audit、tool 日志归档、离线评测 |
| RabbitMQ | AMQP、路由灵活、成熟 | 超高吞吐不如 Kafka | Celery broker、异构系统集成 |
| NATS JetStream | 轻、低延迟 Pub/Sub + 持久 | 生态小于 Kafka | 多 Agent 信令、边缘云指令 |
| AWS SQS / GCP Pub/Sub | 免运维 | 延迟与语义各异 | Serverless Agent、事件入云 |
组合范式:
- Redis :
XREADGROUP执行 Run(抢任务)+XREAD推 SSE。 - Kafka :同一事件 异步双写(fire-and-forget)做审计与 BI。
- 不要 用 Kafka 替代 Redis 做 分布式锁 或 毫秒 session。
16.4 工作流与定时:长跑 Agent 的「编排层」
| 软件 / 包 | 替代 Redis 的哪块 | 说明 |
|---|---|---|
| Temporal | ZSET 调度、复杂重试、多步 DAG | 长跑 Coding Agent、人审等待、版本化 Workflow |
| Celery + Redis/RabbitMQ | List/Stream 队列 | Python 生态熟;子任务多时常够用 |
| Inngest / Trigger.dev | 事件驱动 + 延迟 | SaaS 友好,少运维 |
| Airflow / Prefect | 批处理定时 | 适合离线评测流水线,非在线 Agent 热路径 |
| APScheduler + DB | 简单 Cron | 单进程调度,多副本需 Leader |
Redis 仍适合:「30 秒后重试工具」 、会话 TTL ;Temporal 适合:「等人审批 3 天再继续」。
16.5 Python / Node 常用包(与 Redis 协作)
| 包 | 作用 | 与 Redis 关系 |
|---|---|---|
| redis(redis-py) | 官方客户端 | 本文主示例 |
| redis.asyncio | 异步池 | FastAPI / asyncio Agent |
| celery | 分布式任务 | broker=redis:// 或 amqp:// |
| arq | 轻量 async 队列 | 基于 Redis,比 Celery 简 |
| rq | Redis Queue | 极简后台任务 |
| dramatiq | 任务队列 | broker:Redis / RabbitMQ |
LangGraph langgraph-checkpoint-redis |
Agent 图状态 | 与自建 Hash checkpoint 二选一 |
LangGraph langgraph-checkpoint-postgres |
图状态持久 | 更可审计;Redis 做 cache |
| GPTCache | 语义缓存框架 | 底层可接 Redis / 向量库 |
Socket.IO redis-adapter |
多节点 WebSocket | Pub/Sub 同步房间;与 Stream tail 互补 |
| BullMQ(Node) | 队列 | Redis 原生;Node Agent 栈 |
16.6 状态与记忆:不要只用 Redis
| 存储 | 适合存什么 | 与 Redis |
|---|---|---|
| PostgreSQL / SQLite | Run 元数据、审批记录、计费 | Redis 缓存 + PG 权威;崩溃可重建 |
| DynamoDB / Cosmos | Serverless 会话 | 无运维;成本高延迟略高 |
| 向量库(Qdrant、pgvector) | Embedding、语义缓存 | Redis 存 query hash → 指向 vector id |
| 图数据库 | 实体关系(少见) | 与 Agent 记忆论文相关,非默认栈 |
LangChain / LangGraph 常见写法 :checkpoint 落 Postgres ;工具结果指针落 S3 ;Redis 只管 run 锁、限流、live tail。
16.7 推荐组合(按团队规模)
| 阶段 | 组合 | 说明 |
|---|---|---|
| MVP | Redis 单机 + ConnectionPool + PG |
队列、会话、SSE 一条龙 |
| 成长 | Redis Sentinel + S3 + OTel | 高可用;产物不进 Redis |
| 规模化 | Redis Cluster + Kafka + Temporal + PG | 热冷分离;编排与人审交给 Temporal |
| 重 RAG | Redis + pgvector/Qdrant + GPTCache | 语义缓存分层 |
| Serverless | Upstash Redis + SQS + DynamoDB | 按量付费;注意 XREAD BLOCK 超时计费 |
16.8 何时不必上 Redis
| 情况 | 可考虑 |
|---|---|
| 单进程 Demo | 内存 dict + 本地 SQLite |
| 强一致事务编排 | 直接 Temporal + PG,Redis 只作 cache |
| 仅批处理、无在线 SSE | Kafka + DB,无需 Stream tail |
| 团队零运维 | 托管 Upstash / ElastiCache + 托管 PG |
23. 延伸阅读
| 主题 | 链接 |
|---|---|
| Redis 命令与数据结构 | redis.io/docs |
| redis-py 文档 | redis.readthedocs.io |
| Valkey(Redis 兼容) | valkey.io |
| Temporal(长跑工作流) | docs.temporal.io |
| Agent Harness 综述(本仓库) | csdn-综述-AI-Agent-Harness与Harness-Engineering.md |
| 长跑 Agent 结构化状态 | Anthropic · Effective harnesses for long-running agents |
小结 :Redis 贯穿 前后端 · 端云 · 队列 · 流式 · 定时 · 发布消费 等场景;§14 落地 连接池、TLS、ACL、Key 规范、监控告警与上线 Checklist ;§16 说明与 Kafka/Temporal/PG 等分工。先查 §0b ,生产上线前过一遍 §14.8 ;延伸链接见 §23。