Jev 生产可靠性检查清单
通过问题版本、结构校验、重试、置信度策略、审计日志、评估数据和人工兜底,把 Jev 安全接入生产环境。
1. 给决策契约做版本管理
将模型版本、State Schema、问题 ID、criteria 和阈值策略一起记录。任一部分变化都可能改变行为,并应触发新的评估。
const decisionContract = {
model: 'typesafe-ai/jev',
stateSchema: 'support-ticket@3',
questions: 'routing@5',
policy: 'confidence-bands@2',
}; 2. 区分调用失败与模型不确定
超时、限流、过载和无效响应需要重试或降级;低置信度不是网络错误,而是应该进入明确复核策略的有效信号。
3. 记录足够的可复现信息
保存请求 ID、规范化 State 哈希、问题版本、答案、概率分布、置信度、延迟和最终动作,同时避免泄露敏感原文。
4. 逐步评估和放量
先跑离线标注集,再影子运行生产流量,统计人工覆盖和错误成本,并提供一键停用开关。只有指标满足策略后才扩大自动化。
动手验证:完整示例与失败路径
本课会在你指定的本地 SQLite 文件中写入演示工单、决策与投递记录。请使用新的练习目录,不要指向业务数据库;它不会调用模型、发送邮件或执行支付。
这是本站编写的合成输入与模拟响应实验,Python 3.11+ 标准库即可运行,不需要密钥,不联网。测试通过只证明程序按预期处理这些输入,不能证明 Jev 的准确率、延迟或真实服务重试行为。
失败策略要能执行
这里把评估、保存决策和执行动作分开。模拟 provider 只回答 Choice,不接触数据库;evaluate 给每次尝试 20ms、总计 70ms、最多两次。503/429/超时可以在预算内重试;401、错误选项和无效分数直接复核。这些短时间仅为了快速故障演练,不是生产建议或 Jev 实测延迟。
- ingest:用工单 ID + 版本绑定输入摘要。已保存的请求直接复用结果,同一键配不同输入会拒绝。模型调用在事务外进行,事务内再次检查并同时保存决策与 outbox。
- deliver:在同一 SQLite 事务中写入 applied_events、更新本地工单分配并标记 outbox 已投递。提交前故障会一起回滚,重启后再投递仍只提交一次;旧版本不会覆盖新版本。
- 并发:两个 worker 仍可能重复调用模型,但唯一键与事务阻止两个持久化动作。数据库锁等待有 5 秒上限;锁错误应交给作业层退避,而不是回到模型无限重试。
故障演练应看到什么
| 场景 | 预期结果 |
|---|---|
| transient | 首次 503,第二次成功;attempts=2,仅一条 outbox。 |
| auth / invalid / timeout | 进入 review;auth 与 invalid 只尝试一次,timeout 最多两次,不产生分配。 |
| crash-before-commit | 故意抛错后 inspect 的 assignments 和 applied_events 都为空;恢复后 deliver 输出 applied=1,再运行输出 0。 |
边界:这个保证仅覆盖同一个 SQLite 事务里的本地工单更新。邮件、支付和外部 API 不会随数据库回滚;不能把“先发送,再标记”叫 exactly-once。外部系统必须支持稳定幂等键,或使用可核对的投递记录与补偿流程。挂起的协程也必须配合取消;真实 HTTP 适配器需要自己的连接和读取超时,并避免 SDK 与外层各重试两次形成倍增。
下载离线回归测试 · 下载测试所需的类型实验 · SQLite 事务语义 ↗
展开完整 SQLite 实验源码 · reliability_lab.py
"""Offline reliability lab: bounded evaluation + SQLite outbox + local effect.
Python 3.11+, standard library only. No provider, key, payment or email call.
The deterministic fixtures teach application behavior, not model accuracy.
"""
import argparse
import asyncio
from contextlib import contextmanager
import hashlib
import json
import os
import sqlite3
import time
import sys
from math import isfinite
if sys.version_info < (3, 11):
raise SystemExit('This lab requires Python 3.11 or newer; check python3 --version before running it.')
class ProviderError(Exception):
def __init__(self, status):
self.status = status
@contextmanager
def connect(path):
db = sqlite3.connect(path, timeout=5, isolation_level=None)
db.row_factory = sqlite3.Row
db.executescript("""
CREATE TABLE IF NOT EXISTS decisions (
request_id TEXT PRIMARY KEY, input_hash TEXT NOT NULL, result TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS outbox (
event_id TEXT PRIMARY KEY, ticket_id TEXT NOT NULL, version INTEGER NOT NULL, team TEXT NOT NULL,
delivered INTEGER NOT NULL DEFAULT 0);
CREATE TABLE IF NOT EXISTS applied_events (event_id TEXT PRIMARY KEY);
CREATE TABLE IF NOT EXISTS assignments (ticket_id TEXT PRIMARY KEY, version INTEGER NOT NULL, team TEXT NOT NULL);
""")
try:
with db:
yield db
finally:
db.close()
def fingerprint(ticket_id, version, message):
body = json.dumps([ticket_id, version, message], ensure_ascii=False).encode()
return hashlib.sha256(body).hexdigest()
def cached(db, request_id, input_hash):
row = db.execute("SELECT * FROM decisions WHERE request_id=?", (request_id,)).fetchone()
if row and row["input_hash"] != input_hash:
raise ValueError("Idempotency key was reused for a different input")
return json.loads(row["result"]) if row else None
def valid_route(answer):
c = answer.get("confidence") if isinstance(answer, dict) else None
return (isinstance(answer, dict) and answer.get("type") == "choice"
and answer.get("choice") in {"technical", "billing", "human"}
and isinstance(c, (int, float)) and not isinstance(c, bool)
and isfinite(c) and 0 <= c <= 1)
async def evaluate(provider, *, attempt_timeout=0.02, total_timeout=0.07, max_attempts=2):
"""Provider must be an async callable with no application side effects."""
deadline = time.monotonic() + total_timeout
attempts = 0
reason = "budget_exhausted"
while attempts < max_attempts:
remaining = deadline - time.monotonic()
if remaining <= 0:
break
attempts += 1
try:
answer = await asyncio.wait_for(provider(attempts), min(attempt_timeout, remaining))
except TimeoutError:
reason = "timeout"
except ProviderError as error:
reason = "provider_failure"
if error.status not in {429, 500, 502, 503, 504}:
break
except Exception:
reason = "provider_failure"
break
else:
if not valid_route(answer):
reason = "invalid_response"
break
if answer["choice"] == "human" or answer["confidence"] < 0.75:
return {"action": "review", "reason": "policy", "attempts": attempts}
return {"action": "queue_assignment", "team": answer["choice"], "attempts": attempts}
if attempts < max_attempts:
# Bounded demonstration backoff; a live adapter must also bound Retry-After.
await asyncio.sleep(min(0.005, max(0, deadline - time.monotonic())))
return {"action": "review", "reason": reason, "attempts": attempts}
async def ingest(path, ticket_id, version, message, provider, **budget):
request_id = f"{ticket_id}:{version}"
digest = fingerprint(ticket_id, version, message)
with connect(path) as db:
existing = cached(db, request_id, digest)
if existing is not None:
return existing
result = await evaluate(provider, **budget)
with connect(path) as db:
db.execute("BEGIN IMMEDIATE")
# Another worker may have saved a decision while we awaited the provider.
existing = cached(db, request_id, digest)
if existing is not None:
return existing
db.execute("INSERT INTO decisions VALUES (?, ?, ?)", (request_id, digest, json.dumps(result)))
if result["action"] == "queue_assignment":
db.execute("INSERT INTO outbox(event_id,ticket_id,version,team) VALUES (?,?,?,?)",
(request_id, ticket_id, version, result["team"]))
return result
def deliver(path, *, crash_before_commit=False, hard_crash_before_commit=False):
"""The demonstrated side effect stays INSIDE this same SQLite transaction."""
applied = 0
with connect(path) as db:
db.execute("BEGIN IMMEDIATE")
for row in db.execute("SELECT * FROM outbox WHERE delivered=0 ORDER BY event_id").fetchall():
fresh = db.execute("INSERT OR IGNORE INTO applied_events VALUES (?)", (row["event_id"],)).rowcount
if fresh:
db.execute("INSERT INTO assignments VALUES (?, ?, ?) ON CONFLICT(ticket_id) DO UPDATE SET version=excluded.version, team=excluded.team WHERE excluded.version > assignments.version",
(row["ticket_id"], row["version"], row["team"]))
applied += 1
if hard_crash_before_commit:
# Fault drill in a disposable database: bypass Python rollback/cleanup.
os._exit(86)
if crash_before_commit:
raise RuntimeError("Injected crash: local update and marker must roll back together")
db.execute("UPDATE outbox SET delivered=1 WHERE event_id=?", (row["event_id"],))
return applied
def snapshot(path):
with connect(path) as db:
return {name: [dict(row) for row in db.execute(f"SELECT * FROM {name}")]
for name in ("decisions", "outbox", "applied_events", "assignments")}
def fixture(scenario):
async def provider(attempt):
if scenario == "timeout":
await asyncio.sleep(1)
if scenario == "auth":
raise ProviderError(401)
if scenario == "transient" and attempt == 1:
raise ProviderError(503)
if scenario == "invalid":
return {"type": "choice", "choice": "invented", "confidence": 0.99}
return {"type": "choice", "choice": "technical", "confidence": 0.8}
return provider
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("command", choices=["ingest", "deliver", "inspect"])
parser.add_argument("--db", required=True, help="Use a disposable local SQLite file")
parser.add_argument("--scenario", default="ok", choices=["ok", "transient", "timeout", "auth", "invalid"])
parser.add_argument("--ticket", default="ticket-7")
parser.add_argument("--version", type=int, default=1)
parser.add_argument("--crash-before-commit", action="store_true")
parser.add_argument("--hard-crash-before-commit", action="store_true",
help="Fault drill: terminate this process with exit 86 before SQLite commit")
args = parser.parse_args()
if args.command == "ingest":
result = asyncio.run(ingest(args.db, args.ticket, args.version, "Checkout is unavailable", fixture(args.scenario)))
elif args.command == "deliver":
result = {"applied": deliver(args.db, crash_before_commit=args.crash_before_commit,
hard_crash_before_commit=args.hard_crash_before_commit)}
else:
result = snapshot(args.db)
print(json.dumps({"offline_fixture": True, "result": result}, indent=2))
if __name__ == "__main__":
main()
运行
python3 --version # Requires Python 3.11+
python3 reliability_lab.py ingest --db ./jev-lab.sqlite --scenario transient
# Same key: reuses the cached decision; does NOT call the auth fixture.
python3 reliability_lab.py ingest --db ./jev-lab.sqlite --scenario auth
# Fresh key: actually exercises 401 and returns review.
python3 reliability_lab.py ingest --db ./jev-lab.sqlite --ticket auth-case --scenario auth
# Expected injected failure; inspect must show no applied assignment:
python3 reliability_lab.py deliver --db ./jev-lab.sqlite --crash-before-commit
python3 reliability_lab.py inspect --db ./jev-lab.sqlite
python3 reliability_lab.py deliver --db ./jev-lab.sqlite
python3 reliability_lab.py deliver --db ./jev-lab.sqlite
python3 reliability_lab.py ingest --db ./jev-lab.sqlite --ticket invalid-case --scenario invalid
python3 reliability_lab.py ingest --db ./jev-lab.sqlite --ticket timeout-case --scenario timeout如何判断是否运行正确
在新的练习目录运行上面命令;故障命令预期返回非零。重跑 ingest 会复用已有记录,不会重置数据库。下载两个实验及测试文件后运行 python3 test_depth_labs.py,覆盖跨进程重启、事务回滚、并发去重、旧版本及错误输入。