今天要学习 DeepSeek Harness 如何把网页中的一条指令,变成模型请求、真实工具操作,再把执行过程送回界面。我们沿用"运行测试,失败就修复文件"的例子,按相同格式拆清每个对象的作用、输入、内部处理、输出和缺失后果。

小炎: 我先试着说一遍:Client 生成 Session 和 system prompt,发送给 Remote 服务器。Remote 按 sessionId 找到对应 Service,Controller 检查之后找到 Agent。Agent 循环请求模型、调用工具,同时保存 Session,最后把答案传回 Client。这个方向对吗?
大老师: 你抓住了"用户输入 → 执行 → 显示结果"的方向,但中间有四个分界需要修正。
Client 构建的是提交命令;普通网页提交并不在 Client 侧装配 system prompt。Remote 位于 Client,是远程方法的调用入口;真正接收网络请求的是 Host,也就是运行 Agent、管理会话和工具的执行端。Gateway 按接口入口找到 Service,业务层再按 sessionId 找到 Agent。Session 则贯穿整个执行过程,持续记录事实。
还有一个容易忽略的地方:提交接口先返回"已接纳",最终答案通过持续订阅返回。它们是两条返回路径。
我们先固定一个场景:网页已经打开会话 s_123,你发送:
text
请运行测试,如果失败就修复 src/math.ts,然后告诉我结果。
为了让输入输出有具体形状,下面使用这些示例值:
text
sessionId = s_123 本次操作的会话
requestId = req_42 本次用户提交
rpcId = rpc_7 本次网络调用
mode = queue 排入后续 turn
cwd = /workspace/demo 工作目录
provider/model = deepseek/deepseek-chat
当前可见工具 = bash、read、edit
这些值与后面的测试输出用于教学,并非一次实际运行的录制。模型也不一定会采用我们列出的行动顺序。
小炎: 我需要先知道对象是什么。尤其是 Service,它为什么夹在 Gateway 和 Agent 之间?
大老师: Service 是注册到 Cordis 中、供其他组件调用的能力对象。Cordis 是 Harness 使用的组件装配框架;它让组件可以按名称取得能力、接收事件,并管理启动与释放。
比如 Session Controller 是处理会话相关外部命令的 Service;LLM Service 提供模型调用能力。Service 是一种角色,不是某条请求,也不是某个 Session。
再补齐两个相关名词。Cordis 的 Context 是组件取得服务、注册贡献和监听事件的运行入口。Harness 的 Scope 是能力可见范围,决定一个 Agent 能使用哪些工具、指令贡献以及相关监听。它们支持整条链,不能画成用户消息依次穿过的三个关卡。
| 对象 | 作用 | 输入(本例) | 内部处理 | 输出(本例) | 没有它会怎样 |
|---|---|---|---|---|---|
| Cordis Context | 连接组件与所需能力 | 服务注册、监听,以及调用者所在的运行环境 | 按当前上下文取得服务,管理贡献和监听的生命周期 | 调用者可以取得 llm、agents 等能力 |
需要由其他机制承担依赖连接与生命周期管理 |
| Cordis Service | 提供可复用的能力入口 | 所属 Context、名称、配置和方法参数 | 提供具体能力;不同 Service 的实现不同 | 例如 sessionController.prompt(request) |
对应能力必须由调用者自行实现或通过其他入口提供 |
| Harness Scope | 限定 Agent 的能力视野 | Agent 所在范围及其可见注册 | 决定装配时可见的指令、工具和贡献 | 本例可见 bash、read、edit |
缺少范围机制时,难以让不同 Agent 使用不同能力组合 |
所以并不存在"根据 Session 找一个通用 Service,再让 Service 找 Agent"的统一流程。这里调用的具体 Service 是 Session Controller,它提供处理会话命令的方法。
小炎: 那能不能像网络协议一样分层?我不想把所有名字挤进一条箭头。
大老师: 可以按职责分层,但这是一张阅读地图,不是每层都会装配一次模型 prompt 的协议栈。
| 层 | 解决的问题 | 主要对象 |
|---|---|---|
| L1 用户提交 | 如何把人的意图变成客户端动作 | UI、Client Session、Prompt command |
| L2 远程调用 | 如何把方法调用送到执行端 | Remote、Descriptor、Connection、RPC envelope |
| L3 Host 接入 | 请求是否符合协议,应该交给哪个能力 | Host Connection handler、Gateway、GatewayInvocation |
| L4 业务接纳 | 目标会话能否接收这条输入 | Session Controller、命令处理器、Agent 解析器与 Registry |
| L5 Agent 调度 | 何时处理输入,何时继续模型---工具循环 | Inbox、Agent、Turn、Step |
| L6 请求形成 | 本次模型究竟看到什么 | PromptAssembly、各类 Projection、请求准备与构建 |
| L7 外部执行 | 怎样调用模型,怎样执行工具 | LLM Adapter、Model、工具执行管线 |
| L8 返回显示 | 怎样把执行事实和状态呈现给人 | follow、control、Client model、UI |
另外,Session 是 L4--L8 都会使用的事实层。它既为下一次模型请求提供历史,也为界面提供可回放的记录。
小炎: 从点击发送开始,Client 到底构建了什么?sessionId 也是这时生成的吗?
大老师: 本例操作的是已有会话,所以复用 s_123。会话标识在创建会话时确定;当前创建接口允许调用方提供,也允许 Host 生成。每次发送消息并不会再创建一个 Session。
网页先组织一条 Prompt command。这里的 command 是"请把这份输入交给指定会话"的命令数据,不是发给模型的完整 prompt。
json
{
"sessionId": "s_123",
"requestId": "req_42",
"mode": "queue",
"content": [
{
"type": "text",
"text": "请运行测试,如果失败就修复 src/math.ts,然后告诉我结果。"
}
]
}
| 对象 | 作用 | 输入(本例) | 内部处理 | 输出(本例) | 没有它会怎样 |
|---|---|---|---|---|---|
| UI | 接收人的操作 | 输入文本、当前会话、发送按钮点击 | 处理输入状态并发起 Client 调用 | session.prompt(...) |
人需要通过终端或程序等其他入口提交 |
| Client Session | 管理界面端的会话镜像与提交状态 | s_123 和输入内容 |
登记待确认提交,调用 Remote,随后用 Host 更新维护镜像 | 本地 pending 状态与远程提交调用 | UI 必须自行协调提交、确认和事件更新 |
| Prompt command | 明确提交的目标和语义 | 会话、提交标识、发送模式、文本 | 它是数据;由 Client 组织,由 Host 读取 | 上面的 JSON 数据 | 执行端无法明确这条输入属于哪个会话、按何种模式接收 |
Client Session 是界面镜像,Host 的 Core Session 是执行事实。 名字都有 Session,职责却不同;本地显示"已发送"也不等于 Host 已完成任务。
小炎: Remote 不是服务器,那为什么我能调用 remote.session.prompt()?
大老师: 因为 Remote 把远程接口包装成客户端可以调用的方法。RPC,也就是远程过程调用,解决的是"调用另一端的方法"这个问题。Remote 提供方法门面,Connection 提供传输。
Descriptor 是接口描述数据,说明方法对应哪个入口、参数怎样编码、结果怎样解码等。Endpoint 是接口入口名称;本例是 session/prompt。
| 对象 | 作用 | 输入(本例) | 内部处理 | 输出(本例) | 没有它会怎样 |
|---|---|---|---|---|---|
| Client Remote | 把客户端方法调用转换成远程调用 | remote.session.prompt(command) |
根据 Descriptor 准备参数和 endpoint | 向 Connection 调用 session/prompt,参数放在 args 中 |
Client 需要手工维护远程接口约定 |
| Remote Descriptor | 描述两端共同遵守的接口契约 | prompt 方法的声明 |
它是描述数据;调用器按它编码参数、定位方法、处理结果 | endpoint、参数名及结果规则 | 手写参数与服务器方法更容易不一致 |
| Connection / RPC caller | 承担通信及响应配对 | endpoint、参数和取消信号 | 生成 rpcId,发送请求,检查响应与 id;连接体系还管理持续流与连接状态 |
请求发往 Host,返回对应调用结果 | 同样的业务调用需要另一种传输实现 |
| ClientRequest envelope | 给网络请求一个可验证的外壳 | rpc_7、session/prompt、参数 |
它是数据;传输层序列化,Host 解析 | 带 type、rpcId、method、payload 的 JSON |
两端缺少统一的请求类型和配对规则 |
本例的网络请求可表示为:
text
POST api/session/prompt
Content-Type: application/json
json
{
"type": "client-request",
"rpcId": "rpc_7",
"method": "session/prompt",
"payload": {
"args": {
"request": {
"sessionId": "s_123",
"requestId": "req_42",
"mode": "queue",
"content": [
{
"type": "text",
"text": "请运行测试,如果失败就修复 src/math.ts,然后告诉我结果。"
}
]
}
}
}
}
网页传输器使用相对路由;这里展示的是本例的 endpoint 与参数结构。requestId 标识用户提交,rpcId 标识一次传输调用,它们不能混用。
小炎: 那类似 FastAPI 的服务器部分是谁?为什么检查完网络请求,还要再检查一次?
大老师: 最接近服务器路由职责的是 Host 的接入处理器、Gateway 和 Controller 这一组,但它们分别解决不同问题。
接入层处理认证与网络格式;Gateway 按接口契约把请求转成方法调用;Controller 判断业务上能否接收。JSON 合法不意味着会话存在,方法名正确不意味着内容非空。
| 对象 | 作用 | 输入(本例) | 内部处理 | 输出(本例) | 没有它会怎样 |
|---|---|---|---|---|---|
| Host Connection handler | 接收并检查网络请求 | HTTP 请求、JSON 外壳、调用方信息 | 按接入配置检查认证、路由、内容类型、外壳和 method 一致性 | 可交给 Gateway 的调用数据 | 协议错误和业务逻辑会混在一起 |
| Typert Gateway | 按接口契约派发调用 | endpoint=session/prompt 与命名参数 |
查 Descriptor,准备参数,解析对应 Service 与方法 | 调用 Session Controller 的 prompt(request, signal) |
接入层必须自行实现契约检查和派发 |
| GatewayInvocation | 携带一次调用的上下文 | 调用方、endpoint、取消信号等 | 保存并提供本次调用的身份与取消信息 | 被调用方法所需的调用上下文 | 方法难以关联调用来源及本次调用的取消状态 |
| Session Controller | 提供会话 API 入口 | 解码后的 command 与 signal | 委派给负责命令、历史或状态的内部处理器 | 本例得到命令处理器的接纳结果 | 外部调用需要其他会话业务入口 |
| Session Command Controller | 实施提交的业务准入 | s_123、req_42、content、mode |
检查内容,解析 Agent,处理重复提交和附件准入;构造用户消息并调用 followup 或 steer |
输入进入 Agent;返回 { accepted: true } |
合法网络请求仍无法按会话业务规则执行 |
这里要记住两种查找:
text
endpoint = session/prompt → 调哪个 Service 方法?
sessionId = s_123 → 这个方法操作哪个 Session / Agent?
Gateway 并不是拿 s_123 去决定调用 Session Controller 还是 LLM Service。接口入口已经说明了本次要调用什么能力。
小炎: "找到 Agent"又涉及 Registry、SessionStore 和持久化,它们有什么区别?
大老师: 它们分别管理执行者、已打开的会话和磁盘上的记录。首先定义 Agent :它是协调输入、模型调用与工具执行的运行对象。Core Session 则记录这个运行过程中的事实,二者不能互相代替。
| 对象 | 作用 | 输入(本例) | 内部处理 | 输出(本例) | 没有它会怎样 |
|---|---|---|---|---|---|
| API Agent 解析器 | 把会话 id 解析成可操作的 Agent | s_123 |
查询 live Agent;必要时协调从保存的会话恢复,并协调重复恢复请求 | 当前可操作的 agent(s_123),或明确失败 |
Controller 需要自己处理已有 Agent 与恢复路径 |
Agent Registry / agents |
管理进程中的 Agent | Agent id、创建或恢复参数 | 保存 live Agent,提供查找、创建、恢复等能力 | 对应 Agent 实例 | 业务层无法统一定位与管理执行者 |
SessionStore / sessions |
管理已打开的 Core Session | 会话 id 与会话创建、打开请求 | 管理内存中的 Session 生命周期,并与持久化能力协作 | session(s_123) |
运行中的会话需要其他管理机制 |
| Session persistence | 保存和读取会话记录 | Session 日志、元数据、会话标识 | 将记录保存到持久介质,支持后续读取与恢复 | 重启后仍可读取的记录 | 进程结束后不能仅靠内存找回会话 |
| Core Session | 保留执行事实并提供不同视图 | 用户消息、模型消息、工具结果、运行边界等 | 追加事件,维护当前有效消息视图与请求相关记录 | 历史事件、模型消息投影、界面历史来源 | 下一步模型与界面缺少共同的事实依据 |
本例首先尝试找到已经运行的 Agent,不能把"根据 sessionId 找到 Agent"理解成"寻找之前某个 HTTP 请求并继续执行"。HTTP 调用会结束,会话可以继续存在。
SessionStore 也不能简单定义为磁盘数据库;管理已打开对象与保存对象是两个职责。
小炎: Agent 已经找到了,为什么接口只返回 accepted,不直接返回修复结果?
大老师: 因为接纳输入和完成任务之间,可能隔着排队、多次模型调用、多次工具执行、等待用户或失败。
Inbox 是 Agent 的待处理输入队列。queue 通过 followup() 把消息安排给后续 Turn ;Turn 是一轮任务执行的边界。一轮里可以包含多个 Step;Step 是循环中一次模型调用及其工具处理的执行边界。一次 Step 还可能因为重试包含多个模型请求尝试。
| 对象 | 作用 | 输入(本例) | 内部处理 | 输出(本例) | 没有它会怎样 |
|---|---|---|---|---|---|
| ReactLoopInbox | 安排输入在哪个边界生效 | 带 req_42 来源的用户消息,目标 next-turn |
记录排队输入;到相应边界由 Agent 领取 | 本轮领取的用户消息 | 运行中纠偏与后续任务容易混在一起 |
| ReactLoopAgent | 协调模型---工具循环 | Inbox、Session、可用能力和运行配置 | 调度 Turn/Step,形成请求,处理模型流与工具,决定继续或结束 | Session 事实与执行状态变化 | 请求与工具之间缺少统一调度者 |
| Turn | 表示一轮执行的开始和结束 | 本轮输入与运行状态 | Agent 记录 turn 边界并循环 Step | 一轮执行的完成、阻塞或中断等结果 | 难以区分"这轮结束"与"某次模型调用结束" |
| Step | 表示一次循环推进 | 已领取输入、当前历史和能力 | Agent 形成请求并处理模型/工具结果;必要时重试 | 助手消息、工具结果、Step 结束状态 | 无法清楚说明何时重新装配、何时只是重试 |
Turn 和 Step 是执行边界,不能因为它们在对象表中出现,就假定各自都是一个长期 Service 实例。
业务接纳成功后,RPC 的成功值是:
json
{ "accepted": true }
它表示该提交已进入目标 Agent 的输入路径;重复的已接纳提交也可能得到同样确认。它不承诺"测试通过"或"修复成功"。Agent 可以在确认响应返回途中已经开始执行,因此也不能保证所有执行事件都在网页收到 accepted 之后才到达。
小炎: 我想先把这段画对。accepted 应该接在 Agent 前面吗?
大老师: 应该画成返回 Client 的分支。Agent 处理的是用户消息,不是拿着 accepted 继续调用模型。
这个图只画提交路径。把订阅和模型循环也全部塞进来,会再次让人误以为它们都在等待这一次 HTTP 返回。
小炎: 接下来以 prompt 装配为主线。每一层是不是都会装配 prompt,只是装配内容不同?
大老师: 并不是。Client 组织命令,Remote 组织远程参数,Connection 组织网络外壳。它们转换的是提交的表示形式。真正面向模型的装配在 Agent 的 Step 中发生。
PromptAssembly 是一次装配产生的材料对象。Plugin 是装入运行环境、注册能力或贡献的组件;Agent 向当前 Scope 中可见的贡献收集材料:
text
sections:行为指令段落 → system prompt
contexts:当前运行状态 → 运行上下文消息
tools:工具名称、说明和参数结构 → 模型可见工具定义
variables:段落中需要代入的值 → 渲染时使用
比如本例可以收集到工作目录 /workspace/demo、编码指令,以及 bash、read、edit 的工具定义。收集结果并不是已经发给供应商的 HTTP 请求。
这里的 Projection,投影,指把一组数据转换成某种用途的表示。例如把装配材料变成需要记录的上下文消息,或把 Session 的当前有效历史变成模型消息。
| 对象或方法 | 作用 | 输入(本例) | 内部处理 | 输出(本例) | 没有它会怎样 |
|---|---|---|---|---|---|
systemPrompt.assemble() / PromptAssembly |
汇集本 Step 可见的材料 | Agent Scope、已注册贡献、当前环境 | 收集并排序指令与上下文,收集工具和变量 | 四类材料的装配结果 | 动态能力难以反映到当前模型视野 |
| RuntimeContextProjection | 将运行状态变化转成消息 | 工作目录等当前快照与旧快照 | 比较内容;相同去重,变化或清空时产生相应表示 | 必要的 runtime-context 用户消息 | 环境变化可能丢失,或每步重复堆入相同内容 |
agent/pre-step 扩展点 |
在进入 Step 前调整或拒绝 | 已领取用户消息、候选上下文消息、运行位置 | 可由注册组件改写消息或拒绝本次进入 | 允许进入时的消息与装配材料 | 统一的 Step 准入与改写需要放到其他位置 |
renderPrompt() |
把指令材料变成文字 | 已排序 sections 与 variables | 插值、过滤空内容、连接段落 | 本次 system 文本 | 结构化段落尚不能作为模型指令使用 |
prepareRequest() / PreparedLlmCall |
确定模型与适配器能力 | 路由配置、turn/step、取消信号 | 经过请求扩展点,准备 provider/model 与适配器 | 已解析配置、可调用的适配器及能力信息 | 后续无法可靠决定请求该怎样交给哪个模型 |
| SystemPromptProjection | 决定 system 如何进入事实与请求视图 | system 文本、旧记录、适配器能力 | 决定追加、替换、清除或不变 | 必要的 system/message 提交 |
system 的变化难以按当前模型协议记录和投影 |
buildRequest() / Harness Request |
形成内部模型请求 | 模型配置、工具、Session 有效历史 | 记录必要的请求配置变化,取得消息和工具定义历史,构造请求 | messages、tools、toolHistory、配置与 signal | 装配材料还没有变成实际可调用的请求 |
本例对应的关键顺序是:
text
领取 Inbox 输入
→ 收集 PromptAssembly
→ 计算运行上下文变化
→ agent/pre-step 决策
→ 开始 Step,渲染 system 文本
→ prepareRequest,确定路由与适配器
→ 按适配器能力记录 system,首次尝试追加本 Step 新消息
→ buildRequest
→ 调用模型
表中的 prepareRequest()、buildRequest() 是 Agent 的方法;PromptAssembly、PreparedLlmCall、Request 是数据或调用准备结果。它们不是一批独立服务器。
小炎: Session 在这里提供什么?是不是把完整日志直接塞进 prompt?
大老师: Session 提供当前有效的模型消息视图,不把所有运行事件原样发送给模型。
Surface 是 Session 当前有效消息视图的结构。deriveMessages() 从它导出模型消息;压缩等机制可以改变有效视图,同时保留原始日志。模型需要用户、助手和工具反馈,不需要把每个内部调度事件当成聊天文本。
另外,toolHistory() 这个名字很容易误导:它记录的是历史工具定义及增删变化,供适配器投影能力使用。执行过哪些工具、返回了什么,则通过相应消息和结果进入 deriveMessages() 的历史。
本例第一次请求的模型视野,可以简化成:
text
system:Agent 行为指令与当前可见的工具使用指导
user:请运行测试,如果失败就修复 src/math.ts,然后告诉我结果。
user:当前运行上下文,例如 cwd=/workspace/demo
tools:bash、read、edit 的定义
config:provider/model 等模型配置
这是帮助理解的内容示意,不是某个供应商的逐字段 HTTP 格式。实际指令文本与消息投影方式取决于当前贡献和适配器。
没有 Session 提供共同事实层,模型下一步看到的历史与网页展示的历史就需要分别维护,更容易出现"界面显示过,模型却不知道"或"模型用过,恢复后却找不到依据"的问题。
小炎: 请求发出之后,模型和工具各自做什么?模型返回 bash 就等于命令已经执行了吗?
大老师: 模型返回工具调用,表示提出行动。真正运行命令的是 Harness 的工具执行能力。
LLM Adapter 是模型协议适配器,把 Harness 的统一请求转换成供应商协议,并将返回的数据归一化。Tool call 是模型给出的工具名称与参数。Sandbox 是操作系统层面的执行限制;审批和 guard 则是执行前的策略检查。这些角色都不能与模型的判断混为一谈。
| 对象 | 作用 | 输入(本例) | 内部处理 | 输出(本例) | 没有它会怎样 |
|---|---|---|---|---|---|
| LLM Adapter | 隔离模型供应商协议差异 | Harness Request,选择 DeepSeek | 转换消息与工具,发送请求,将返回归一化 | 异步 StreamChunk 流 |
Agent 必须直接理解每家供应商协议 |
| Model | 在当前视野内决定下一步 | 指令、历史、工具定义 | 生成文本或结构化行动请求 | 可能给出 bash 调用,命令为 pnpm test |
需要其他机制选择行动并生成回答 |
| Tool guard / Approval / Sandbox | 判断并限制可执行行动 | tool call、参数、策略与执行环境 | 检查准入;策略需要时请求审批;按环境限制执行 | 允许、拒绝或等待,以及受限执行条件 | 行动缺少相应策略检查或系统约束 |
| Tool Executor | 执行已准入的工具 | bash 调用、取消信号 |
调用真实工具实现,收集结果 | 例如退出码 1 和测试失败输出 | 模型只提出动作,文件与进程不会因此改变 |
假设工具实际返回:
text
command: pnpm test
exitCode: 1
output: src/math.ts:8 expected 3 received 4
Agent 把助手的工具调用与工具结果记录到 Session。注意,工具成功运行、测试通过、用户目标完成,是三个不同判断:命令可以成功启动,却报告测试失败。
小炎: 那 Step 2 会收到什么?是不是把工具结果放进 Inbox,再拼一次完整 prompt?
大老师: 工具结果进入 Session 后,下一次请求就能通过模型消息投影看到它。继续循环并不要求 Inbox 一定出现新消息。
Inbox 管理新输入何时生效;Session 保留已经发生的对话与执行事实。某些工具还会另外提供补充上下文,运行中的用户也可能发来 steer,这些才可能成为下一 Step 要领取的输入。
| 比较项 | Step 1 | Step 2 |
|---|---|---|
| 新领取的输入 | 本例的用户指令 | 可以为空;也可能有纠偏或补充上下文 |
| 历史事实 | 原有会话历史及本次已接纳的模型输入 | 增加 Step 1 的助手工具调用和测试结果 |
| 装配材料 | 收集当前指令、环境、工具、变量 | 再次收集,以反映可能变化的能力和环境 |
| 模型配置 | 准备本次 provider/model 与能力 | 重新准备;可受请求路由影响 |
| 本次可能输出 | 请求执行 pnpm test |
结合失败信息,可能先读取 src/math.ts 再判断修改 |
这里的"重新装配"不意味着把相同的 system 和环境消息无限追加。未变化的内容会通过相应记录与投影机制复用或去重;新请求仍会形成完整的当前视野。对本例而言,最关键的新内容是工具反馈。
还要区分 新 Step 和 同一 Step 的重试。当前实现对一个已进入的 Step 使用同一份装配材料;每次模型请求尝试会重新准备和构建请求,本 Step 新领取的消息只在首次尝试追加,避免因为重试复制用户输入。
这是普通执行主线的简图,省略了失败、阻塞、取消与重试分支。Session 在旁边持续参与,工具结果也没有被画成必经 Inbox 的输入。
小炎: 最后模型回答出来,为什么还需要 follow、control 和 Client model?
大老师: 因为界面需要持续观察执行过程,而一次提交 RPC 已经结束。还需要同时处理历史、正在生成的输出,以及会话状态。
Stream,流 ,是持续送来多条更新的返回方式。Frame,帧 ,是一条流中的更新单位。Cursor,游标,说明历史读取到哪里,帮助断线后补齐。Client model 再把这些更新折叠成 UI 可以使用的状态。
| 对象 | 作用 | 输入(本例) | 内部处理 | 输出(本例) | 没有它会怎样 |
|---|---|---|---|---|---|
| Session 事件与观察机制 | 将执行事实提供给观察者 | 用户、助手、工具及运行边界的变化 | Session 记录事实,相关观察器提供历史与变化;持久化能力另行负责保存 | 可回放的记录与更新通知 | 界面和恢复逻辑缺少统一变化来源 |
session.follow |
跟随一个会话的历史与实时助手输出 | 会话 id、历史位置、订阅取消信号 | 提供会话记录,并承载助手实时流帧 | 测试调用与结果记录,以及正在生成的回答片段 | 需要另一种会话跟随与历史补齐机制 |
session.control |
跟随 Host 范围的会话状态投影 | 控制订阅与当前会话投影 | 提供状态基线和后续投影变化 | 当前哪些会话存在、是否运行等状态 | 界面需要其他机制维护 Host 范围的会话状态 |
| Client model / Projection | 把更新变成界面状态 | 历史事件、实时帧、状态变化 | 合并更新,维护历史位置与本地视图 | 可渲染的消息、工具卡片与运行状态 | UI 必须逐个处理原始协议更新 |
| Conversation / UI | 展示执行过程和结果 | Client 的会话视图 | 渲染文本、工具过程和状态 | 人看到测试失败、检查文件、后续回答 | 执行仍可发生,但人没有这一展示入口 |
所以不能把所有实时助手输出都划给 control。当前 follow 也承载助手的实时流帧;control 关注 Host 范围的会话状态投影。取消 Agent 是另一个命令,不是"关闭流就取消任务"。
这里画的是数据来源和返回职责,不代表每次提交都要新开两个流。界面可以已有订阅,且响应与流更新的网络到达顺序不能当成业务顺序。
小炎: 那我把原来的 11 步改成下面这一版,应该怎样说才完整?
大老师: 可以这样向别人讲:
- 网页把用户文本整理成指定会话的提交命令,并显示待确认状态。
- Client Remote 根据接口描述,把方法调用转换成 endpoint 与参数。
- Connection 添加网络调用标识和请求外壳,发往 Host。
- Host 接入层检查认证和协议格式,Gateway 按 endpoint 找到 Service 方法。
- Session Controller 的命令处理逻辑检查内容与业务条件,并按 sessionId 解析目标 Agent。
- 用户消息按 queue 或 steer 语义进入 Agent 的输入路径;接纳结果返回 Client。
- Agent 在相应执行边界领取新输入,开启或继续 Turn/Step。
- 每个新 Step 收集当前能力与上下文,结合 Session 有效历史形成模型请求。
- LLM Adapter 调用模型;模型提出工具调用时,Harness 检查并执行。
- 用户输入、模型输出、工具反馈和运行事实持续进入 Session,Agent 据此决定继续或结束。
- 已建立的订阅把历史、实时输出和状态送到 Client,Client 更新界面。
其中第 6 步的"已接纳"和第 11 步的"最终答案"是不同返回。对于终端、SDK 或其他程序入口,前面的 Client/Remote/Gateway 路径可能不同;这里讲的是普通网页调用,不能要求所有入口都照搬这一组对象。
小炎: 所以 Harness 不只是"把 prompt 发给模型",还要把哪些责任分开?
大老师: 至少要分开提交表示、传输契约、业务接纳、执行调度、事实记录和观察显示。每一层的输出只完成自己的职责:网络成功不等于业务接纳,业务接纳不等于模型完成,模型完成也不等于测试通过。
总结:从调用顺序看责任边界

一条用户指令先成为 Client 命令,再成为远程方法参数和网络请求;Host 将其派发给业务 Service,业务层把输入交给目标 Agent。Agent 按 Turn/Step 调度,结合当前能力与 Session 历史构造模型请求。模型的行动请求由工具执行能力落实,反馈记录到 Session,并影响后续 Step。界面通过持续订阅观察事实与状态。
整条链中,endpoint 选择能力入口,sessionId 选择执行会话,requestId 标识用户提交,rpcId 配对网络调用。Session 是共同事实层,Client 是它的界面镜像;accepted 是接纳确认,最终消息另行返回。
可以迁移到其他系统的一条设计认识是:接收任务、执行任务、记录事实和展示进度需要清楚的责任边界。判断一次调用"成功"之前,先说明它确认了哪一层的成功。
源码复查入口(当前工作区版本):
- Client Remote 与 网页 RPC caller:参数包装与网络外壳。
- Host Gateway 与 Session 命令处理:Service 派发、Agent 解析与接纳。
- Agent loop 与 工具调用处理:Step 装配、请求尝试与结果记录。
- Core Session、follow 历史处理 与 control 状态处理:模型历史、工具定义历史及返回流职责。
学习进度:本文整理和校正 N03 的调用边界,N03 仍为"讲解中";不新增 C14"模型请求装配"的学习进度。后续候选仍为 N04"执行世界的能力图"。