问题与目标
Notebook 中一次 invoke() 成功,不等于 Agent 已经成为服务。Runtime 还要管理任务身份、并发、事件流、取消、恢复和资源回收。本篇用 FastAPI 搭建一个最小运行时骨架,重点展示线程隔离和 SSE 生命周期。
核心概念

run_id 标识一次执行,thread_id 标识可恢复的任务上下文,user_id 标识认证主体。三者不能混用:同一 Thread 可包含多次 Run,但只能由授权主体访问。
Runtime 中建议统一事件格式:
{"event":"tool_started","run_id":"run-1","thread_id":"task-9","seq":2,"data":{"tool":"get_status"}}
seq 用于断线后去重或补发;事件 payload 不应包含密钥、完整 Prompt 或未脱敏工具结果。
可运行实现
python -m pip install "fastapi>=0.115,<1" "uvicorn>=0.30,<1"
import asyncio
import json
import uuid
from dataclasses import dataclass, field
from fastapi import FastAPI, Header, HTTPException
from fastapi.responses import StreamingResponse
app = FastAPI()
@dataclass
class Task:
owner: str
run_id: str
queue: asyncio.Queue = field(default_factory=asyncio.Queue)
cancel: asyncio.Event = field(default_factory=asyncio.Event)
status: str = "running"
tasks: dict[str, Task] = {}
def owned_task(thread_id: str, user_id: str) -> Task:
task = tasks.get(thread_id)
if not task or task.owner != user_id:
raise HTTPException(404, "task not found")
return task
async def execute(thread_id: str, task: Task) -> None:
for seq, step in enumerate(("plan", "get_status", "summarize"), start=1):
if task.cancel.is_set():
task.status = "cancelled"
await task.queue.put({"event": "cancelled", "seq": seq})
break
await task.queue.put({"event": "step_started", "seq": seq, "data": {"step": step}})
await asyncio.sleep(0.2) # 替换为支持超时的真实节点
else:
task.status = "completed"
await task.queue.put({"event": "completed", "seq": 4})
await task.queue.put(None)
@app.post("/tasks/{thread_id}")
async def start(thread_id: str, x_user_id: str = Header()) -> dict:
if thread_id in tasks:
raise HTTPException(409, "thread already exists")
task = Task(owner=x_user_id, run_id=str(uuid.uuid4()))
tasks[thread_id] = task
asyncio.create_task(execute(thread_id, task))
return {"thread_id": thread_id, "run_id": task.run_id}
@app.get("/tasks/{thread_id}/events")
async def events(thread_id: str, x_user_id: str = Header()):
task = owned_task(thread_id, x_user_id)
async def stream():
while True:
item = await task.queue.get()
if item is None:
break
item.update({"thread_id": thread_id, "run_id": task.run_id})
yield f"data: {json.dumps(item, ensure_ascii=False)}\n\n"
return StreamingResponse(stream(), media_type="text/event-stream")
@app.post("/tasks/{thread_id}/cancel")
async def cancel(thread_id: str, x_user_id: str = Header()) -> dict:
task = owned_task(thread_id, x_user_id)
task.cancel.set()
return {"status": "cancelling"}
运行 uvicorn app:app --reload 后先创建任务,再连接事件接口。示例使用内存对象教学;生产环境必须把 Thread、Checkpoint 和事件日志放入共享持久存储,否则多进程或重启后无法恢复。
取消通常是协作式的:Runtime 设置取消标记,节点在安全位置检查。对外部 HTTP 调用同时设置超时并传播取消;已经提交的第三方写操作不能假装撤销,应通过幂等查询或补偿流程确认状态。
常见问题与排查
用户可以猜测别人的 Thread ID
每次读取、恢复、取消都按认证主体校验所有权,未授权时返回统一的不存在响应,避免泄露任务存在性。
SSE 断开后任务也停止
执行任务和网络订阅解耦。事件写入持久日志,客户端带最后一个 seq 重连并补发,不能把浏览器连接当任务生命周期。
进程重启后显示仍在运行
启动时扫描租约超时的 Run,结合 Checkpoint 标记为可恢复或失败。恢复前重验副作用是否已经发生。
只设置 Agent 总超时
同时设置模型、工具、数据库和整个任务的分层超时。错误中标明阶段,便于决定重试、恢复还是人工处理。
小结
Agent Runtime 把一次模型调用变成可服务化任务。线程所有权、持久事件、协作式取消和 Checkpoint 恢复是同一套生命周期管理,不能只补一个 SSE 接口就算完成。
License: CC BY-NC 4.0
Updated 2 hours ago
Was this article helpful? Give it a like.
0 comments


