【封闭结束1】端到端交付:Agent 一次请求如何走完全链路

前言 :封闭告一段落,开始系统复盘:一个生产 Agent 到底怎么跑起来。踩坑、经验和落地,最主要的是用户认可。尽量用几篇文章讲清,这篇文章是系列第1篇:用一次只读查询,把从界面到 Trace 的整条链路先跑通。

用户说:"查一下生产环境 order-service 最近的错误日志。"

最终只看到一段回答,背后至少经过身份、会话、意图、模型、工具、流式事件、持久化和观测。任何一跳出错,都可能表现成"Agent 答错了"。这篇先把整条链路跑通,后面几篇再分头细说。

案例 OpsPilot(企业运维助手,虚构):

  • 只读工具:search_resource、query_logs
  • 知识工具:search_knowledge_base
  • 写工具:restart_instance,必须 HITL(Human-in-the-Loop,人在回路确认)
  • 技术栈:Node.js + TypeScript + Vue 3

一张总图

#mermaid-svg-jKENiVaUknBYSX9N{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jKENiVaUknBYSX9N .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jKENiVaUknBYSX9N .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jKENiVaUknBYSX9N .error-icon{fill:#552222;}#mermaid-svg-jKENiVaUknBYSX9N .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jKENiVaUknBYSX9N .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jKENiVaUknBYSX9N .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jKENiVaUknBYSX9N .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jKENiVaUknBYSX9N .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jKENiVaUknBYSX9N .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jKENiVaUknBYSX9N .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jKENiVaUknBYSX9N .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jKENiVaUknBYSX9N .marker.cross{stroke:#333333;}#mermaid-svg-jKENiVaUknBYSX9N svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jKENiVaUknBYSX9N p{margin:0;}#mermaid-svg-jKENiVaUknBYSX9N .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jKENiVaUknBYSX9N .cluster-label text{fill:#333;}#mermaid-svg-jKENiVaUknBYSX9N .cluster-label span{color:#333;}#mermaid-svg-jKENiVaUknBYSX9N .cluster-label span p{background-color:transparent;}#mermaid-svg-jKENiVaUknBYSX9N .label text,#mermaid-svg-jKENiVaUknBYSX9N span{fill:#333;color:#333;}#mermaid-svg-jKENiVaUknBYSX9N .node rect,#mermaid-svg-jKENiVaUknBYSX9N .node circle,#mermaid-svg-jKENiVaUknBYSX9N .node ellipse,#mermaid-svg-jKENiVaUknBYSX9N .node polygon,#mermaid-svg-jKENiVaUknBYSX9N .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jKENiVaUknBYSX9N .rough-node .label text,#mermaid-svg-jKENiVaUknBYSX9N .node .label text,#mermaid-svg-jKENiVaUknBYSX9N .image-shape .label,#mermaid-svg-jKENiVaUknBYSX9N .icon-shape .label{text-anchor:middle;}#mermaid-svg-jKENiVaUknBYSX9N .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jKENiVaUknBYSX9N .rough-node .label,#mermaid-svg-jKENiVaUknBYSX9N .node .label,#mermaid-svg-jKENiVaUknBYSX9N .image-shape .label,#mermaid-svg-jKENiVaUknBYSX9N .icon-shape .label{text-align:center;}#mermaid-svg-jKENiVaUknBYSX9N .node.clickable{cursor:pointer;}#mermaid-svg-jKENiVaUknBYSX9N .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jKENiVaUknBYSX9N .arrowheadPath{fill:#333333;}#mermaid-svg-jKENiVaUknBYSX9N .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jKENiVaUknBYSX9N .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jKENiVaUknBYSX9N .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jKENiVaUknBYSX9N .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jKENiVaUknBYSX9N .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jKENiVaUknBYSX9N .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jKENiVaUknBYSX9N .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jKENiVaUknBYSX9N .cluster text{fill:#333;}#mermaid-svg-jKENiVaUknBYSX9N .cluster span{color:#333;}#mermaid-svg-jKENiVaUknBYSX9N div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jKENiVaUknBYSX9N .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jKENiVaUknBYSX9N rect.text{fill:none;stroke-width:0;}#mermaid-svg-jKENiVaUknBYSX9N .icon-shape,#mermaid-svg-jKENiVaUknBYSX9N .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jKENiVaUknBYSX9N .icon-shape p,#mermaid-svg-jKENiVaUknBYSX9N .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jKENiVaUknBYSX9N .icon-shape .label rect,#mermaid-svg-jKENiVaUknBYSX9N .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jKENiVaUknBYSX9N .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jKENiVaUknBYSX9N .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jKENiVaUknBYSX9N :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} POST + SSE
Vue 对话 UI
会话服务
鉴权与 RequestContext
Agent Runtime
意图与上下文组装
模型网关
LLM Provider
Tool Runtime
业务 API / MCP
RAG 检索
向量库 / 全文索引
Run 事件与消息持久化
Trace / Metrics / Eval

图上每一块管一件事,别混:

  • 模型负责读懂意思,并给出结构化提案。
  • Runtime 负责推进步骤、卡预算、处理中断。
  • Tool Runtime 负责 schema(参数长什么样)、鉴权、幂等(同一操作做一次和做多次)和真正执行。
  • 会话服务负责身份、Run 生命周期和跟前端的协议。
  • Trace/Eval 负责事后说清「到底哪一跳错了」。

请求怎么进 Runtime

会话 API 入口

前端不应直连模型。它调用自己的会话 API:

bash 复制代码
POST /api/conversations/conv_123/runs
Authorization: Bearer <user-token>
Content-Type: application/json
Idempotency-Key: 2d7e45b8

{
  "message": "查一下生产环境 order-service 最近的错误日志"
}

RequestContext 从哪来

服务入口中间件从可信登录态拼出上下文。尤其是 tenantId 和 userId,别从模型参数或用户原文里读:

ts 复制代码
export type RequestContext = { // 入口鉴权后的请求上下文
  requestId: string   // 本请求 ID,日志与排障用
  traceId: string     // 全链路追踪 ID,跨会话/工具/模型共用
  tenantId: string    // 租户;检索过滤与数据隔离以此为准
  userId: string      // 当前用户;工具鉴权读这里,不读模型参数
  roles: string[]     // 角色,用于 ACL / RAG 可见范围
  scopes: string[]    // 允许调用的工具或资源范围
  deadlineAt: number  // 整次 Run 截止时间戳,超时取消
}

会话服务创建 runId,写入用户消息,然后把 Run 交给 Runtime。HTTP 连接只是订阅进度,不是任务状态本身。

只读时序:从提案到回灌

场景 :用户问「查一下生产环境 order-service 最近的错误日志」。

这是只读链路,不需要 HITL;通常会先定位资源,再查日志,最后给出带证据的回答。按谁干什么读图:

参与方 职责
VueUI 发起 POST,订阅 SSE,渲染步骤与增量文本
SessionAPI 验身份、建 Run、转发事件;不直连模型
AgentRuntime 推进 ReAct:调模型、派工具、收结果、决定是否结束
ModelGateway 统一调用 LLM,返回 tool_calls 或最终文本
ToolRuntime 校验参数、按 RequestContext 鉴权、执行业务 API
Observability 记录 span、版本、Token,供事后归因

Observability ToolRuntime ModelGateway AgentRuntime SessionAPI VueUI User Observability ToolRuntime ModelGateway AgentRuntime SessionAPI VueUI User #mermaid-svg-rbBNoBelapuNva4V{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rbBNoBelapuNva4V .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rbBNoBelapuNva4V .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rbBNoBelapuNva4V .error-icon{fill:#552222;}#mermaid-svg-rbBNoBelapuNva4V .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rbBNoBelapuNva4V .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rbBNoBelapuNva4V .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rbBNoBelapuNva4V .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rbBNoBelapuNva4V .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rbBNoBelapuNva4V .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rbBNoBelapuNva4V .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rbBNoBelapuNva4V .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rbBNoBelapuNva4V .marker.cross{stroke:#333333;}#mermaid-svg-rbBNoBelapuNva4V svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rbBNoBelapuNva4V p{margin:0;}#mermaid-svg-rbBNoBelapuNva4V .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-rbBNoBelapuNva4V text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-rbBNoBelapuNva4V .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-rbBNoBelapuNva4V .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-rbBNoBelapuNva4V .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-rbBNoBelapuNva4V .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-rbBNoBelapuNva4V #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-rbBNoBelapuNva4V .sequenceNumber{fill:white;}#mermaid-svg-rbBNoBelapuNva4V #sequencenumber{fill:#333;}#mermaid-svg-rbBNoBelapuNva4V #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-rbBNoBelapuNva4V .messageText{fill:#333;stroke:none;}#mermaid-svg-rbBNoBelapuNva4V .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-rbBNoBelapuNva4V .labelText,#mermaid-svg-rbBNoBelapuNva4V .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-rbBNoBelapuNva4V .loopText,#mermaid-svg-rbBNoBelapuNva4V .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-rbBNoBelapuNva4V .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-rbBNoBelapuNva4V .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-rbBNoBelapuNva4V .noteText,#mermaid-svg-rbBNoBelapuNva4V .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-rbBNoBelapuNva4V .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-rbBNoBelapuNva4V .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-rbBNoBelapuNva4V .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-rbBNoBelapuNva4V .actorPopupMenu{position:absolute;}#mermaid-svg-rbBNoBelapuNva4V .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-rbBNoBelapuNva4V .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-rbBNoBelapuNva4V .actor-man circle,#mermaid-svg-rbBNoBelapuNva4V line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-rbBNoBelapuNva4V :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 查询 order-service 错误日志POST runstart(run, context)SSE run_startedmessages + tool schemassearch_resource(args)tool_startedSSE tool_startedinvoke(search_resource, args, context)exact resourceToolMessage(result)query_logs(args)invoke(query_logs, args, context)structured log summaryToolMessage(result)final answerspans + metrics + versionsfinal messageSSE text_delta + done

切记:每个 tool_call_id 必须有对应结果。工具失败也要返回结构化错误,否则下一轮模型既无法自我修正,也可能违反模型 API 的消息约束。

关键几跳:

  1. 入口 :前端 POST run,会话服务带着可信 RequestContext 启动 Runtime,同时先推 run_started,让 UI 立刻有反馈。
  2. 第一轮模型 :Runtime 把消息和工具 schema 交给模型。模型提出 search_resource------此时还没查库,只是「提案」。
  3. 工具执行 :Runtime 发 tool_started 给前端,再交给 Tool Runtime 真正调用。身份用上下文里的 tenantId/userId,不信模型参数里的身份字段。
  4. 结果回灌 :工具结果变成 ToolMessage 再进模型。每个 tool_call_id 都必须有一条结果;失败也要结构化回灌,否则下一轮对不齐消息协议。
  5. 第二轮工具 :模型根据资源信息再提 query_logs,同样走 Tool Runtime,拿到结构化日志摘要。
  6. 收尾 :模型给出最终自然语言答案;Runtime 落 Trace,会话服务推 text_delta + done。HTTP 断开不代表 Run 不存在,状态以会话服务为准。

一句话:模型只提案、Tool Runtime 才执行;前端只订事件、别自己猜「是不是在调工具」;写操作别走这条「连续自动跑」的路,要进下一节的 HITL。

事件、写操作 HITL 与落库

前端、落库和观测最好围着同一组业务事件转。事件类型可以先定成下面这一组:

ts 复制代码
export type RunEvent = // Run 业务事件联合
  | {
      type: 'run_started'    // 事件:Run 已开始
      runId: string          // 本次 Run ID
      sequence: number       // 事件序号,去重/续传用
    }
  | {
      type: 'text_delta'     // 事件:增量文本
      runId: string          // 所属 Run
      sequence: number       // 事件序号
      delta: string          // 增量文本,不是全量覆盖
    }
  | {
      type: 'tool_started'   // 事件:工具开始执行
      runId: string          // 所属 Run
      sequence: number       // 事件序号
      callId: string         // 与 tool_finished 配对
      tool: string           // 工具名,给步骤条展示
    }
  | {
      type: 'tool_finished'  // 事件:工具结束
      runId: string          // 所属 Run
      sequence: number       // 事件序号
      callId: string         // 对应 tool_started
      status: 'success' | 'error' // 执行结果状态
      summary: string        // 给前端的短摘要,不是原始大结果
    }
  | {
      type: 'interrupt'      // 事件:HITL 等待确认
      runId: string          // 所属 Run
      sequence: number       // 事件序号
      interruptId: string    // 恢复时带回,防止过期确认
      payload: unknown       // 确认卡内容:风险说明、参数摘要等
    }
  | {
      type: 'done'           // 事件:Run 正常结束
      runId: string          // 所属 Run
      sequence: number       // 事件序号
      messageId: string      // 落库后的助手消息 ID
    }
  | {
      type: 'error'          // 事件:Run 失败
      runId: string          // 所属 Run
      sequence: number       // 事件序号
      code: string           // 可分类错误码,勿回灌堆栈
    }

sequence 用来去重和以后补发;第一版就算还没做事件重放,字段也先留着。别让前端靠解析自然语言去猜「现在是不是在调工具」。

写操作为什么必须换一条路

如果用户说:"重启这个异常实例"。模型只能提出动作,不能自己给自己开权限:
#mermaid-svg-4zOCdjmGB3xlrtCd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4zOCdjmGB3xlrtCd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4zOCdjmGB3xlrtCd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4zOCdjmGB3xlrtCd .error-icon{fill:#552222;}#mermaid-svg-4zOCdjmGB3xlrtCd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4zOCdjmGB3xlrtCd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4zOCdjmGB3xlrtCd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4zOCdjmGB3xlrtCd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4zOCdjmGB3xlrtCd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4zOCdjmGB3xlrtCd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4zOCdjmGB3xlrtCd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4zOCdjmGB3xlrtCd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4zOCdjmGB3xlrtCd .marker.cross{stroke:#333333;}#mermaid-svg-4zOCdjmGB3xlrtCd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4zOCdjmGB3xlrtCd p{margin:0;}#mermaid-svg-4zOCdjmGB3xlrtCd defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-4zOCdjmGB3xlrtCd g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-4zOCdjmGB3xlrtCd g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-4zOCdjmGB3xlrtCd g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-4zOCdjmGB3xlrtCd g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-4zOCdjmGB3xlrtCd g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-4zOCdjmGB3xlrtCd .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-4zOCdjmGB3xlrtCd .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-4zOCdjmGB3xlrtCd .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-4zOCdjmGB3xlrtCd .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-4zOCdjmGB3xlrtCd .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-4zOCdjmGB3xlrtCd .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-4zOCdjmGB3xlrtCd .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-4zOCdjmGB3xlrtCd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4zOCdjmGB3xlrtCd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4zOCdjmGB3xlrtCd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4zOCdjmGB3xlrtCd .edgeLabel .label text{fill:#333;}#mermaid-svg-4zOCdjmGB3xlrtCd .label div .edgeLabel{color:#333;}#mermaid-svg-4zOCdjmGB3xlrtCd .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-4zOCdjmGB3xlrtCd .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-4zOCdjmGB3xlrtCd .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-4zOCdjmGB3xlrtCd .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-4zOCdjmGB3xlrtCd .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-4zOCdjmGB3xlrtCd .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4zOCdjmGB3xlrtCd .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4zOCdjmGB3xlrtCd #statediagram-barbEnd{fill:#333333;}#mermaid-svg-4zOCdjmGB3xlrtCd .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4zOCdjmGB3xlrtCd .cluster-label,#mermaid-svg-4zOCdjmGB3xlrtCd .nodeLabel{color:#131300;}#mermaid-svg-4zOCdjmGB3xlrtCd .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-4zOCdjmGB3xlrtCd .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-4zOCdjmGB3xlrtCd .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-4zOCdjmGB3xlrtCd .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-4zOCdjmGB3xlrtCd .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-4zOCdjmGB3xlrtCd .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-4zOCdjmGB3xlrtCd .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-4zOCdjmGB3xlrtCd .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-4zOCdjmGB3xlrtCd .note-edge{stroke-dasharray:5;}#mermaid-svg-4zOCdjmGB3xlrtCd .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-4zOCdjmGB3xlrtCd .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-4zOCdjmGB3xlrtCd .statediagram-note text{fill:black;}#mermaid-svg-4zOCdjmGB3xlrtCd .statediagram-note .nodeLabel{color:black;}#mermaid-svg-4zOCdjmGB3xlrtCd .statediagram .edgeLabel{color:red;}#mermaid-svg-4zOCdjmGB3xlrtCd #dependencyStart,#mermaid-svg-4zOCdjmGB3xlrtCd #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-4zOCdjmGB3xlrtCd .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4zOCdjmGB3xlrtCd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} restart_instance proposed
approved + reauthorized
rejected or expired
Running
WaitingHuman
Cancelled
Completed
Failed

恢复时至少重做四件事:

  • 校验 interruptId 仍 pending;
  • 按当前 RequestContext 重新鉴权;
  • 重新读取资源,防止确认期间状态变化;
  • 用幂等键避免双击和网络重试造成重复执行。

具体实现见后面几篇;多轮、SSE、HITL 和工具 Runtime 会分开写。

Run 最少要存什么

SSE 可能断,Run 还在跑。所以库里至少要有一张「状态小票」RunRecord:随时能查这笔请求卡在哪、用了哪版 Prompt/模型。它不是聊天全文,也不是工具吐出来的大结果。

ts 复制代码
type RunRecord = { // Run 落库最小字段
  id: string                 // Run 主键
  conversationId: string     // 所属会话
  tenantId: string           // 租户隔离字段,查询必带
  status:                   // Run 生命周期状态
    | 'queued'               // 已入队,等待 Worker
    | 'running'              // 模型/工具执行中
    | 'waiting_human'        // HITL 等待确认
    | 'succeeded'            // 正常结束
    | 'failed'               // 失败结束
    | 'cancelled'            // 用户或系统取消
  lastSequence: number       // 已发出的最大事件序号(断线续传/去重)
  promptVersion: string      // 本轮 Prompt 版本,排障/回滚用
  toolsetVersion: string     // 工具集版本
  model: string              // 实际调用的模型名
  createdAt: string          // 创建时间
  updatedAt: string          // 最后状态变更时间
}

日志摘要可能很长:另存成 artifact(对象存储或附件表),Run 里只留引用。

Prompt、工具集、模型、知识库版本要能挂到 Trace 上;否则线上回退时说不清「当时到底用了哪一版」。

验收、排障与交付顺序

层 可验证的完成标准
Vue UI 流式增量可渲染;工具步骤可折叠;停止与 HITL 可操作
会话服务 用户消息和 Run 落库;断开可查结果;取消语义明确
Runtime 最大步数;重复调用检测;可中断;状态可恢复
Tool Runtime schema、scope、超时、错误分类、幂等与审计
RAG 租户过滤;引用可回原文;文档更新可失效
模型网关 超时与 429 分类;模型版本、Token 和成本可观测
安全 外部内容不改变权限;高风险操作有独立控制点
质量 一条 traceId 可还原轨迹;修改前后可跑同一 Eval
发布 健康检查、灰度、回滚和告警入口可用

一个故障怎么定位

用户反馈:"我明明查生产,为什么返回测试环境?"

按 Trace 找第一个错误点:

ts 复制代码
// Trace 片段:意图对了,但工具参数环境错了------第一错误点在填参,不在库查询
{
  traceId: 'tr_01', // 整次请求追踪 id
  intent: { // 意图分析结果
    type: 'query_logs', // 意图正确
    environment: 'production' // 用户要生产
  },
  toolCalls: [ // 实际发出的工具调用
    {
      name: 'query_logs', // 工具选对了
      args: { // 工具参数
        service: 'order-service', // 服务正确
        environment: 'test' // 实际调成了测试 → 修 Prompt/结构化约束
      }
    }
  ]
}

意图对了、工具参数错了,就修选工具/填参的 Prompt 或结构化约束;别先去改数据库查询。

交付顺序与业务验收

交付顺序没有行业标准。下面只是风险从低到高的建议,不是固定周数:

  1. 会话落库 + 文本 SSE。
  2. 一个只读工具 + ToolMessage 错误回灌。
  3. RAG 权限过滤 + 引用。
  4. 写工具 + HITL + 幂等审计。
  5. Eval 发布检查 + Trace 大盘。
  6. 模型降级、预算、灰度和回滚。

业务侧也不能省。一次完整上线至少能说清:

text 复制代码
需求澄清 → 成功指标 → 只读原型 → 写操作与 HITL → 上线 → 采用率 / 用户反馈 → 回滚与复盘

成功指标先写业务结果(例如「值班能在工作台查完日志并点开引用」),再写模型指标。没有采用率和回滚记录,只能证明系统能跑,不能证明有人在用。

小结

这篇先钉住三件事:

  1. 模型提案,Tool Runtime 执行。
  2. 前端订事件,不猜状态。
  3. 写操作必须 HITL + 重鉴权。

文中的 OpsPilot、ID 和 Trace 都是可复现的示例。下一篇继续:《Agent 意图识别:从语义理解到稳定路由》。

希望这篇文章对你有所帮助、有所借鉴。

相关推荐
小雉系统5 小时前
BcoreOS安全通信架构:FsSession 抗算法后门安全设计
算法·安全·架构
Miya_Ye6 小时前
AI工具 Phoenix
ai·工具
Csvn7 小时前
每请求到底多少钱?把 Token 成本算成可观测指标(C01)
人工智能·aigc·agent
吴名氏.7 小时前
Java转AI Agent教程(第十三周)混合检索与重排
java·数据库·人工智能·ai·ai agent
treasure1408 小时前
【复古】浅尝Django前后端不分离
前端·css·python·django·bootstrap·html·web
jay神9 小时前
基于 YOLOv8 的手势识别系统(Web 版)
前端·人工智能·python·深度学习·yolo·计算机视觉·计算机毕业设计
屏什么录9 小时前
长时间录屏中途崩溃:MP4 打不开的 6 种救回方案与参数设置
前端·音视频开发
code_Bo10 小时前
不只命令行:我把跨 Agent 会话同步做成了桌面应用
前端·javascript·后端
kyriewen10 小时前
AI 能把页面写出来,为什么改个需求就开始崩?
前端·javascript·人工智能
聪明的笨猪猪10 小时前
企业知识库AI架构选型:落地导向的架构决策与组件清单
经验分享·面试·架构·embedding·数据库架构