在 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 流式响应。

相关推荐
边吃番茄边敲代码42 分钟前
企业智能助手Agent 安全测试,包含Prompt Injection 和越权工具调用
人工智能·python·功能测试·ai·单元测试·prompt·模块测试
科技小E43 分钟前
自建团队、买SaaS、还是用自动化AI算法训练服务器DLTM?企业AI视觉三条路线算笔账
人工智能·深度学习·自动化
小鹿软件办公1 小时前
Apple Music 将于 2026 年底强制为 AI 音乐添加“由 AI 制作”标签
人工智能·苹果音乐ai生成标签·apple music
AI模型调用笔记1 小时前
Codex mcp-server 被弃用:不是 MCP 退场,App Server、SDK 与 Claude Code 插件怎么选?
人工智能
liangbo71 小时前
JVM规范第 4 章:class 文件格式
java·jvm
hyuk的AI工坊1 小时前
多模态图文理解:LangChain4j 视觉理解实战(DashScope 通义千问 qwen-vl)
人工智能
橘子汽水1681 小时前
Leetcode 236,437:二叉树的最近公共祖先,路径总和III
java·数据结构
牛油果子哥q1 小时前
C++模板万字深度精讲:函数/类模板特化、全特化与偏特化、SFINAE机制、类型萃取、模板元编程入门、工程实战落地
开发语言·c++·面试
KKKlucifer1 小时前
异构融合与大规模割接——某电信运营商融合4A平台建设实践
大数据·网络·人工智能·安全