在 OpenClaw Java 对话框输入一句话后,系统是如何运行的?

一、前言

在网页端对话框输入一句话并点击发送后,系统并不是简单地把文本转发给大模型,而是会经历一套完整的处理流程:

  1. 前端获取用户输入;
  2. 通过 HTTP SSE 发起聊天请求;
  3. Spring Controller 校验请求和权限;
  4. 创建或恢复会话;
  5. 保存用户消息;
  6. 进入 Agent Runtime;
  7. 准备工作区、上下文和工具;
  8. 选择模型 Provider;
  9. 调用外部大模型;
  10. 根据模型结果执行工具或继续对话;
  11. 将模型输出转换成 SSE;
  12. 前端实时显示回答。

本文按照当前工作区的实际源码,对这条链路进行说明。

注意:当前 openclaw-web 网页对话使用的是 POST /v1/chat/completions,不是 Gateway WebSocket 的 chat.send。不过两条链路最终都会进入同一个 ChatAgentBridgeAgentRunner


二、整体架构图

#mermaid-svg-4hcHtBzA27U5QGZe{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-4hcHtBzA27U5QGZe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4hcHtBzA27U5QGZe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4hcHtBzA27U5QGZe .error-icon{fill:#552222;}#mermaid-svg-4hcHtBzA27U5QGZe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4hcHtBzA27U5QGZe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4hcHtBzA27U5QGZe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4hcHtBzA27U5QGZe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4hcHtBzA27U5QGZe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4hcHtBzA27U5QGZe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4hcHtBzA27U5QGZe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4hcHtBzA27U5QGZe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4hcHtBzA27U5QGZe .marker.cross{stroke:#333333;}#mermaid-svg-4hcHtBzA27U5QGZe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4hcHtBzA27U5QGZe p{margin:0;}#mermaid-svg-4hcHtBzA27U5QGZe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4hcHtBzA27U5QGZe .cluster-label text{fill:#333;}#mermaid-svg-4hcHtBzA27U5QGZe .cluster-label span{color:#333;}#mermaid-svg-4hcHtBzA27U5QGZe .cluster-label span p{background-color:transparent;}#mermaid-svg-4hcHtBzA27U5QGZe .label text,#mermaid-svg-4hcHtBzA27U5QGZe span{fill:#333;color:#333;}#mermaid-svg-4hcHtBzA27U5QGZe .node rect,#mermaid-svg-4hcHtBzA27U5QGZe .node circle,#mermaid-svg-4hcHtBzA27U5QGZe .node ellipse,#mermaid-svg-4hcHtBzA27U5QGZe .node polygon,#mermaid-svg-4hcHtBzA27U5QGZe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4hcHtBzA27U5QGZe .rough-node .label text,#mermaid-svg-4hcHtBzA27U5QGZe .node .label text,#mermaid-svg-4hcHtBzA27U5QGZe .image-shape .label,#mermaid-svg-4hcHtBzA27U5QGZe .icon-shape .label{text-anchor:middle;}#mermaid-svg-4hcHtBzA27U5QGZe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4hcHtBzA27U5QGZe .rough-node .label,#mermaid-svg-4hcHtBzA27U5QGZe .node .label,#mermaid-svg-4hcHtBzA27U5QGZe .image-shape .label,#mermaid-svg-4hcHtBzA27U5QGZe .icon-shape .label{text-align:center;}#mermaid-svg-4hcHtBzA27U5QGZe .node.clickable{cursor:pointer;}#mermaid-svg-4hcHtBzA27U5QGZe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4hcHtBzA27U5QGZe .arrowheadPath{fill:#333333;}#mermaid-svg-4hcHtBzA27U5QGZe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4hcHtBzA27U5QGZe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4hcHtBzA27U5QGZe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4hcHtBzA27U5QGZe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4hcHtBzA27U5QGZe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4hcHtBzA27U5QGZe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4hcHtBzA27U5QGZe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4hcHtBzA27U5QGZe .cluster text{fill:#333;}#mermaid-svg-4hcHtBzA27U5QGZe .cluster span{color:#333;}#mermaid-svg-4hcHtBzA27U5QGZe 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-4hcHtBzA27U5QGZe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4hcHtBzA27U5QGZe rect.text{fill:none;stroke-width:0;}#mermaid-svg-4hcHtBzA27U5QGZe .icon-shape,#mermaid-svg-4hcHtBzA27U5QGZe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4hcHtBzA27U5QGZe .icon-shape p,#mermaid-svg-4hcHtBzA27U5QGZe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4hcHtBzA27U5QGZe .icon-shape .label rect,#mermaid-svg-4hcHtBzA27U5QGZe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4hcHtBzA27U5QGZe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4hcHtBzA27U5QGZe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4hcHtBzA27U5QGZe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

用户在网页输入消息
App.vue submit
chat.sendMessage
streamChat
POST /v1/chat/completions
OpenAiChatController
鉴权与请求解析
SessionStore 创建或查找会话
SessionTranscriptStore 保存用户消息
ChatAgentBridgeImpl
AgentRunner.runAsync
准备 Sandbox 和上下文
加载工具与技能
ModelProviderRegistry
OpenAICompatibleProvider
外部大模型 API
是否需要调用工具
ToolRegistry 执行工具
生成最终回答
OpenAiChatController SSE
浏览器解析 delta
页面显示回答


三、第一步:用户提交消息

前端页面位于:

  • openclaw-web/src/App.vue
  • openclaw-web/src/stores/chat.ts

用户点击发送后,页面调用:

text 复制代码
submit()
    ↓
chat.sendMessage(value, activeSessionKey)

对应流程可以简化为:

ts 复制代码
async function submit() {
  const value = draft.value.trim()
  if (!value) return

  draft.value = ''
  await chat.sendMessage(value, activeSessionKey.value)
}

sendMessage() 会先在前端做三件事:

  1. 把用户消息加入消息列表;
  2. 创建一个空的 assistant 消息作为占位;
  3. 设置 isGenerating = true,表示当前正在生成回答。

此时页面上通常会先显示用户刚刚输入的内容,以及一个正在生成中的空白回答区域。


四、第二步:前端发起 HTTP SSE 请求

当前网页聊天调用的是:

http 复制代码
POST /v1/chat/completions

核心请求体如下:

json 复制代码
{
  "model": "default",
  "sessionKey": "main",
  "messages": [
    {
      "role": "user",
      "content": "你好,请介绍一下这个项目"
    }
  ],
  "stream": true
}

前端通过 fetch() 发起请求:

ts 复制代码
fetch('/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'text/event-stream'
  },
  body: JSON.stringify({ ...request, stream: true })
})

为什么使用 SSE?

因为大模型通常不是一次性返回完整答案,而是逐字或逐段返回。SSE 可以让用户在模型还没有完全生成结束时,就看到已经生成的内容。

请求响应头主要包括:

http 复制代码
Content-Type: text/event-stream
Cache-Control: no-cache, no-transform
Connection: keep-alive

五、第三步:Spring Controller 接收请求

请求进入:

text 复制代码
openclaw-app/src/main/java/com/openclaw/app/openai/OpenAiChatController.java

对应方法:

text 复制代码
chatCompletions()

Controller 首先进行鉴权:

text 复制代码
Authorization: Bearer <gateway-token>
        ↓
extractBearerToken()
        ↓
AuthService.authorize()

如果鉴权失败,接口返回:

json 复制代码
{
  "error": {
    "message": "Unauthorized",
    "type": "authentication_error"
  }
}

HTTP 状态码为:

text 复制代码
401 Unauthorized

如果没有有效的用户消息,则返回:

text 复制代码
400 Bad Request

六、第四步:创建或恢复会话

Controller 会根据 sessionKey 查找当前会话:

text 复制代码
SessionStore.findBySessionKey(sessionKey)

如果查找不到,就创建一个新会话:

text 复制代码
SessionStore.createSession(sessionKey, cwd)

一个会话通常包含以下信息:

字段 说明
sessionId 会话唯一 ID
sessionKey 对外使用的会话 key,例如 main
model 当前会话使用的模型
cwd 当前工作目录
status 会话运行状态
createdAt 创建时间
updatedAt 更新时间

之后 Controller 会更新当前会话使用的模型:

text 复制代码
sessionStore.updateSession(sessionId, session -> {
    session.setModel(model);
})

七、第五步:保存用户消息

用户输入会被保存到 transcript:

text 复制代码
SessionTranscriptStore.appendMessage(
    sessionId,
    "user",
    rawUserMessage
)

消息不会只保存在浏览器内存中,而是会写入 OpenClaw 的会话记录文件。

整体关系如下:

text 复制代码
sessionKey
    ↓
SessionStore
    ↓
sessionId
    ↓
SessionTranscriptStore
    ↓
sessionId.jsonl

这样做的好处是:

  • 页面刷新后可以继续查看历史;
  • Agent 可以读取当前会话上下文;
  • 后续请求可以关联同一个 session;
  • 可以记录 token 用量和运行状态。

八、第六步:进入 ChatAgentBridge

Controller 不直接调用模型,而是把请求交给:

text 复制代码
ChatAgentBridge.runChat()

实现位于:

text 复制代码
openclaw-app/src/main/java/com/openclaw/app/bridge/ChatAgentBridgeImpl.java

桥接层主要负责:

  1. 解析模型 ID;
  2. 处理模型别名;
  3. 把 OpenAI 消息转换成内部消息对象;
  4. 组装 AgentRunContext
  5. 调用 AgentRunner.runAsync()

可以把 Bridge 理解成 HTTP 层和 Agent Runtime 之间的适配层:

text 复制代码
HTTP ChatCompletionRequest
        ↓
ChatRunRequest
        ↓
AgentRunContext
        ↓
AgentRunner

这样可以让 HTTP 接口、Gateway WebSocket 接口和其他内部调用共享同一套 Agent 执行能力。


九、第七步:AgentRunner 开始执行

AgentRunner 是真正执行 Agent 逻辑的核心组件,位于:

text 复制代码
openclaw-agent/src/main/java/com/openclaw/agent/runtime/AgentRunner.java

入口通常是:

text 复制代码
AgentRunner.runAsync(context)

它会异步调用真正的运行逻辑:

text 复制代码
runAsync()
    ↓
CompletableFuture.supplyAsync()
    ↓
run(context)

这样可以避免长时间占用 HTTP 请求线程。


十、第八步:准备 Agent 上下文

AgentRunner 开始执行后,不会立即把用户原文发送给模型,而是先准备运行环境。

主要准备内容包括:

1. Sandbox

根据当前配置和会话信息准备 Sandbox 环境,用于隔离文件、命令和工具操作。

2. 工作区上下文

读取项目工作区相关内容,例如:

  • Bootstrap 文件;
  • Agent 指令;
  • 用户配置;
  • 工具说明;
  • Memory 文件;
  • Skills 信息。

3. 系统提示词

系统提示词会和用户消息组合成完整的 Agent 上下文。

最终发送给模型的内容通常类似:

text 复制代码
系统提示词
+ 工作区上下文
+ 会话历史
+ 工具定义
+ 用户消息

所以模型看到的并不只是用户输入的那一句话。


十一、第九步:选择模型 Provider

AgentRunner 根据 modelId 查找模型 Provider:

text 复制代码
ModelProviderRegistry.resolve(modelId)

相关代码位于:

text 复制代码
openclaw-agent/src/main/java/com/openclaw/agent/models/ModelProviderRegistry.java

模型 ID 可能包含 Provider 前缀,例如:

text 复制代码
openai/gpt-4o
anthropic/claude
qwen/qwen-max

Registry 会根据模型 ID 找到对应实现。

常见 Provider 通常会统一适配为 OpenAI 兼容请求格式,由:

text 复制代码
OpenAICompatibleProvider.chatStream()

发起外部请求。


十二、第十步:调用外部大模型 API

Provider 会通过 HTTP 客户端向外部大模型发送请求:

text 复制代码
AgentRunner
    ↓
OpenAICompatibleProvider
    ↓
OkHttp EventSource
    ↓
外部模型 /chat/completions

外部模型通常返回 SSE 数据,例如:

text 复制代码
data: {"choices":[{"delta":{"content":"你好"}}]}

data: {"choices":[{"delta":{"content":",这是一个 Java 项目。"}}]}

Provider 收到每个增量后,会调用内部监听器:

text 复制代码
listener.onText(delta)

然后把文本增量继续传给上层 AgentRunner。


十三、第十一步:模型可能调用工具

模型并不一定直接回答用户问题。

例如用户输入:

text 复制代码
查看当前项目有哪些 Java 模块

模型可能先返回工具调用请求:

json 复制代码
{
  "tool": "exec",
  "arguments": {
    "command": "dir"
  }
}

此时流程变成:

text 复制代码
模型返回 tool call
    ↓
AgentRunner.executeTool()
    ↓
ToolRegistry 查找工具
    ↓
执行工具
    ↓
得到工具结果
    ↓
把工具结果加入对话上下文
    ↓
再次调用模型

完整过程可能是:

text 复制代码
用户 → 模型
模型 → exec 工具
exec 工具 → 返回目录列表
目录列表 → 模型
模型 → 最终回答

可能被调用的工具包括:

  • 文件读取工具;
  • 文件写入工具;
  • Shell/命令工具;
  • 浏览器工具;
  • 搜索工具;
  • 技能工具;
  • 插件工具。

因此,一次用户输入可能对应多次模型调用。


十四、第十二步:Agent 产生文本增量

当模型输出普通文本时,数据会沿着以下方向回传:

text 复制代码
外部模型 delta
    ↓
OpenAICompatibleProvider.onText()
    ↓
AgentRunner listener.onDelta()
    ↓
ChatAgentBridge listener.onDelta()
    ↓
OpenAiChatController listener.onDelta()

Controller 把增量包装成 OpenAI 兼容格式:

json 复制代码
{
  "id": "chatcmpl-uuid",
  "object": "chat.completion.chunk",
  "model": "openclaw",
  "choices": [
    {
      "index": 0,
      "delta": {
        "content": "你好"
      },
      "finish_reason": null
    }
  ]
}

然后通过 SseEmitter 写回浏览器:

text 复制代码
data: {"choices":[{"delta":{"content":"你好"}}]}

十五、第十三步:浏览器解析 SSE 并更新页面

前端 streamChat() 使用:

  • ReadableStream
  • TextDecoder
  • SSE 行解析逻辑。

它会提取:

text 复制代码
data → choices[0] → delta → content

然后调用:

ts 复制代码
onDelta(delta)

Store 再把文本追加到 assistant 消息:

ts 复制代码
const target = messages.value[assistantIndex]
if (target) {
  target.content += delta
}

因此用户可以看到类似这样的实时效果:

text 复制代码
你
你好
你好,这是
你好,这是一个
你好,这是一个 Java
你好,这是一个 Java 项目。

十六、第十四步:请求结束

当 Agent 执行完成后,后端会:

  1. 发送最后一个完成 chunk;
  2. 发送 finish_reason: "stop"
  3. 发送 SSE 结束标记;
  4. 调用 emitter.complete()

结束标记为:

text 复制代码
data: [DONE]

前端读到 [DONE] 后退出读取循环,并将消息状态改为:

text 复制代码
complete

如果中途发生异常,则会进入错误流程。


十七、异常处理流程

1. 鉴权失败

text 复制代码
AuthService.authorize() 失败
    ↓
HTTP 401
    ↓
前端显示鉴权错误

2. 请求参数错误

text 复制代码
messages 缺失或为空
    ↓
HTTP 400
    ↓
前端显示请求参数错误

3. 模型调用失败

text 复制代码
Provider 请求失败
    ↓
AgentRunner 捕获异常
    ↓
onError()
    ↓
SSE 错误 chunk
    ↓
前端显示错误信息

4. 用户主动停止

前端调用 AbortController.abort()

text 复制代码
用户点击停止
    ↓
AbortController.abort()
    ↓
fetch 流被取消
    ↓
前端显示"已停止生成"

5. 工具执行失败

工具失败一般会封装为工具结果返回给 Agent:

text 复制代码
工具执行失败
    ↓
ToolResult.fail()
    ↓
返回模型继续判断

工具失败不一定会导致整次对话立即结束,模型可能根据错误结果继续尝试或向用户说明原因。


十八、会话数据最终保存在哪里

当前系统主要保存两类数据。

1. 会话元数据

用于保存:

  • session ID;
  • session key;
  • 当前模型;
  • 工作目录;
  • 运行状态;
  • 更新时间。

2. 会话消息 transcript

用于保存:

  • 用户消息;
  • assistant 消息;
  • tool 消息;
  • 运行过程中的消息记录。

简化后的持久化结构:

text 复制代码
~/.openclaw/
├── state/
│   └── agents/
│       └── {agentId}/
│           └── sessions/
│               └── sessions.json
└── sessions/
    └── {sessionId}.jsonl

十九、如果使用 Gateway WebSocket,会有什么不同?

系统还保留了 Gateway WebSocket 的聊天入口:

json 复制代码
{
  "type": "req",
  "id": "chat-1",
  "method": "chat.send",
  "params": {
    "sessionKey": "main",
    "message": "你好"
  }
}

这条链路是:

text 复制代码
WebSocket 客户端
    ↓
GatewayWebSocketHandler
    ↓
GatewayMethodRouter
    ↓
ChatMethodHandler.handleChatSend()
    ↓
ChatAgentBridge
    ↓
AgentRunner
    ↓
ModelProvider

区别在于返回方式:

项目 当前网页聊天 Gateway WebSocket
入口 POST /v1/chat/completions WebSocket chat.send
流式协议 HTTP SSE WebSocket event
文本事件 OpenAI chunk chat event
工具事件 隐藏在 HTTP 流程内部 agent event
Agent Runtime 相同 相同
Model Provider 相同 相同

所以两者只有最外层入口不同,底层核心执行逻辑基本一致。


二十、总结

在网页对话框输入一句话后,完整流程可以概括为:

text 复制代码
用户输入
  ↓
Vue 页面
  ↓
HTTP SSE
  ↓
OpenAiChatController
  ↓
鉴权和会话管理
  ↓
ChatAgentBridge
  ↓
AgentRunner
  ↓
上下文、工具、Sandbox
  ↓
ModelProvider
  ↓
外部大模型
  ↓
工具循环或最终回答
  ↓
SSE 增量返回
  ↓
Vue 页面实时显示

最重要的结论有三个:

  1. 当前网页对话入口是 HTTP /v1/chat/completions,不是 Gateway WebSocket;
  2. HTTP 接口内部仍然复用了 Gateway 的会话、鉴权和 Agent 能力;
  3. 一句话可能触发多轮模型调用、工具执行和上下文更新,最终才生成回答。

因此,OpenClaw Java 的一次对话请求,本质上是:

前端流式请求 + Gateway 核心能力 + Agent Runtime + Model Provider + 工具循环 + SSE 流式响应。

相关推荐
AI推荐率12 小时前
GEO发稿套餐中的媒体更换,是否可能产生补差价?
人工智能
user_admin_god12 小时前
第 09 篇:实践一 —— 文本摘要(Map-Reduce 分块)
java·人工智能·spring boot·语言模型
Behaviour12 小时前
豆包手机助手消费者版发布:首款量产 AI 智能体手机 9 月 16 日开售
人工智能·语言模型·aigc·ai编程
prog_610312 小时前
【笔记】用agent手搓agent(一)
人工智能·llm·大语言模型·agent
咖啡八杯12 小时前
Controller 基类封装:BaseController 的模板方法设计
java·架构·设计
vx_BS8133012 小时前
【项目编号:project49759】Spring Boot 宠物综合服务平台:商城、预约、咨询、活动与健康档案的一体化实现
java·spring boot·eclipse·tomcat·intellij-idea
三声三视12 小时前
一个卡片入场动画我返工 4 次:tri-lottie 规格单落地到 ArkTS 的踩坑记录
人工智能·ai·skillhub·tri-skills·tri-lottie
她的男孩12 小时前
加了 @Idempotent 还是重复扣款了 3 笔:扒完 1279 行幂等 Starter,我挖出 5 个隐蔽的坑
java·后端·架构
南京兴帝文化传媒有限公司12 小时前
AI大模型如何抓取和推荐无锡本地商户?GEO技术链路与POI权重算法拆解
大数据·人工智能·生活·geo 优化·ai搜索获客·无锡geo优化·长三角geo优化
是枚小菜鸡儿吖12 小时前
Windows 和 iPhone 怎么互传文件?用 PairDrop 搭一个自己的浏览器传输入口
服务器·人工智能·大模型