A Chronicle · 2013 — 至今

在 ReAct 之前,
我已经在造 Agent 了。

一个 AI 从业者的十三年

这不是一份简历,是一条主线

13
AI / 推荐 / Agent
5
华为·阿里·网易·腾讯·微软
4
一作论文
2
发明专利
2023第一章

在范式之前

Microsoft Edge · Contextual Chat(单页)

2023 年,大模型刚起来,我开始做浏览器里的大模型应用 —— Contextual Chat。 那会儿 ReAct 的论文才刚出,业界还没有「Agent」的范式共识,更没有现成的框架可抄。 但当我把这套系统的架构画出来时,它已经天然长成了后来所有 Agent 都在用的样子。

看那张 2023 的 Overall Architecture

用户的自然语言先进 Triggering (Intent Understanding) 做意图理解, 分流成 Non-ContextualContextual 两条路 —— 这就是后来的 routing / planning。 Contextual 这一路再拆成 Whole Page Summary / Section Summary / Doc QnA, 经 Chunk Search 检索 —— 这就是 retrieval / RAG。 页面内容由 Client Extraction (DU) 抽取、配合 User Consent & Policies 做合规注入 —— 这就是 tool use + grounding。最后落到 Contextual Pipeline (DV3) 生成 —— 这就是 执行

我当时不知道它该叫 Agent,
但它已经是 Agent 了。

两年后的 2025,同一套思想被放大成商用、多标签页的 Copilot。 骨架没变,只是每个器官都长大了 —— 从单页 grounding 变成 multi-tab content and metadata,从一条固定 pipeline 变成可编排的 Sydney Flux Orchestrator

2023 Contextual Chat Overall Architecture 点击放大 ⤢
2023 · 单页
Contextual Chat — Overall Architecture
Triggering → Chunk Search → Client Extraction (DU) → Contextual Pipeline (DV3)。plan→retrieve→tool-use→execute 的骨架,只是那时还叫 Contextual Chat。
2025 Copilot multi-tab architecture 点击放大 ⤢
2025 · 商用 · Multi-tab
Copilot — Sydney Flux Orchestrator
Copilot UX → Sydney Flux Orchestrator ↔ Edge Plugin Runtime ↔ MetaOS Hub ↔ Edge Client。同一思想,放大到多标签页与编排。
2023 · 单页 Contextual Chat
2025 · 商用 Multi-tab Copilot
Triggering (Intent Understanding)
意图分流
Sydney Flux Orchestrator
可编排的 orchestration
Chunk Search + Client Extraction (DU)
单页 grounding
Edge Plugin Runtime / MetaOS Hub
multi-tab content and metadata
Contextual Pipeline (DV3)
单一固定管道
Sydney Flux Orchestrator (+System prompt change)
可编排 flux
User Consent & Policies
合规作为一个模块
user consent
合规贯穿整个架构

这就是我想让你先看到的那个点:我不是追着范式跑,是提前站在了范式将要长出来的地方。 差别只是 2023 还是单页面,2025 变成了 for commercial、for multi-tab。

2023第二章

意图,是一点一点
回忆起来的

Microsoft Edge · Intent Detection 的四次形态

第一章那张图里,最上面那个框叫 Triggering (Intent Understanding)。 整个系统能不能用,就看它能不能准确读懂用户当下想干什么 —— 是要总结这一页、翻译这一段,还是问这张表里的某个数。 这一章讲的,就是这个「意图」是怎么一步步被我榨出来的。 它不是一开始就设计对的,而是被算力逼着,长出了一套越用越聪明的东西

01
LLM based — 直接问大模型

最开始简单粗暴:一个 query 进来,直接用 GPT-3.5 解析出结构化意图 —— Contextual / Non-contextual × Summary / Translate / Rewrite / Question × Page / Section / Table / Figure × Slot。 「总结这一页的第三节」→ Contextual · Summary · Section · 3。干净、直接。 但每一个 query 都要打一次 LLM —— capacity 根本扛不住。

02
ANN based — 会自己长大的缓存

算力不够,就换个打法。先离线用 LLM 给历史 chat log 里的 query 标注意图, 把 query + embedding + meta 灌进一张 ANN table

线上一个新 query 进来,先算 embedding 去 ANN Search 检索: 命中了,就直接取库里那条相似 query 的 meta 结果,不打 LLM; 没命中,才 fallback 到 LLM 现算 —— 然后把这条新的 query + embedding + meta 回写入库

于是这套缓存越用越全,LLM 的调用量随时间被摊薄。 顺带还解掉一个真问题:浅层 embedding 在外语上 recall 差 —— 就用 LLM 生成训练数据去 boost 它。

03
自研 SLM — 干脆自己练一个

开源 embedding 不够好,就自己练了一个 SLM(基于 Mistral SFT)。 这跟同期 Tabs 那条「随效果 / 延迟 / 成本三角持续演进、最后走到自研小模型」的路线是同一个手感 —— 能用大模型的地方先用,扛不住了就往下沉。

04
不是回去,是在 Loop 前再加一层

再后来,这些能力进入了 agentic loop:模型接手一个任务后, 根据需要调用工具,一路推进到结束。但系统入口仍有另一个问题——并不是每个 question, 都值得交给同样昂贵的模型。于是上线了我写的论文 WebRouter: 用变分信息瓶颈(VIB)做 cost-sensitive 的模型路由,在后续推理与执行开始前,先按 question 选择模型。

两者不是替代关系,而是可以相加的两层:WebRouter 回答“先由哪个模型接手”, agentic loop 回答“接手以后,怎样把任务做到底”。 不过边界仍要说清楚:WebRouter 做的是 question-level model routing, 并不是前面那套完整的 intent 系统。

Intent Detection LLM based 点击放大 ⤢
形态 01 · LLM based
Intent Detection — LLM based
query → GPT-3.5 一次性解析出 Task1~4 结构化意图。干净,但每 query 一次 LLM,capacity 扛不住。
Intent Detection ANN based 点击放大 ⤢
形态 02 · ANN based
Intent Detection — ANN based
离线挖掘入库 → 线上 ANN 检索,命中取缓存、未命中打 LLM 并回写。一套越用越全的自扩充缓存。
被算力逼出来的,
往往比一开始就设计好的更聪明。
2023第四章

把检索,
做成一个系统

Microsoft Edge · Longdoc RAG

2023 年做长文档总结,最容易的做法是:切块、embedding、向量检索、丢给大模型。 很多人到今天还停在这一步。但我当时很快就发现 —— 光靠 embedding 不够,中间那一整段「怎么找、找什么、怎么对齐」,本身就是一个需要认真设计的系统。 于是我没有把它当成一次调参,而是当成一个复杂系统来适配大模型。 今天回头看,这其实是很早期的上下文工程

4.1 · 不是调 embedding,是造系统

V1 就暴露了纯 embedding 的两个硬伤:外语 recall 差(缺训练数据), 以及完全不懂文档结构(「第 5.1 节在讲什么」这种问题答不了)。 我的解法不是换个更大的 embedding,而是四管齐下 —— 加关键词匹配召回、给每个 chunk enrich metadata、 用 LLM 自造训练数据做外语增强、把文档结构本身当成 metadata 去增强 chunk 表示。

V2,它长成了一套完整的三路召回系统: Keyword match based + ANN based + Metaword / Metastream based 三条路并行召回,取 Top 15,再交给 Sydney (DV3) 做精排。

召回、粗排、精排、多路融合 ——
这不就是我做了七年的推荐吗?

写到这里我才意识到,这套东西的骨架,和我在腾讯音乐、阿里做推荐的那一套是同构的: 多路召回、metadata 特征工程、粗排到精排的漏斗。 只不过这一次,漏斗最末端接的不是一个排序模型,而是一个大模型。 十年的推荐直觉,在这里直接复用了。

DCS V1 challenges and solution 点击放大 ⤢
V1 · 诊断
Challenges & Solution
纯 embedding 的两个硬伤 → 关键词召回、metadata 增强、LLM 自造 <Q,Chunk> 训练数据、结构入表征。
DCS V2 three-way recall 点击放大 ⤢
V2 · 系统
三路召回 + 精排
Keyword + ANN + Metastream 三路并行 → Top 15 → Sydney (DV3)。一个召回-精排漏斗,末端接大模型。
4.2 · 答案要能指回原文

光找到、生成还不够。大模型说的每一句,得能指回网页里的原文 —— 这就是 In Page Annotation:把 Sydney 生成的句子和页面真实内容逐句做 Similarity Calculation,判断每句 match 与否,再合并连续句、按规则挂上 [2][3][4] 的引用。经过 Pre-process → Recall(cosine + 阈值 Top-K)→ Ranking → Post-processing 四段。

这套引用对齐带来的是硬指标 —— accuracy 从 V1 的 81.6% 提到 V2 的 97.6%, Creative-EN 从 76.3% 提到 93.8%。不是感觉变好了,是量出来的。

In Page Annotation citation alignment 点击放大 ⤢
引用 · 对齐
In Page Annotation
生成句与页面内容逐句相似度比对,match 的挂引用 —— 句级 attribution,不是事后贴角标。
In Page Annotation metrics 点击放大 ⤢
引用 · 指标
Pipeline & Metrics
Pre-process → Recall → Ranking → Post-process;accuracy 81.6%→97.6%,Creative-EN 76.3%→93.8%。

最让我感慨的是:这几年一篇篇会议论文、一个个开源项目, 走的正是我 2023 走过的这条路 —— 大家后来才发现「光靠 embedding 不够」, 要结构感知、要 metadata、要混合召回、要句级引用。摆在一起看:

现代 RAG 技术 · 逐条印证 2023 的选择
建层级目录树 + LLM 推理检索,不用纯向量相似度 —— 「结构感知优于 embedding」的代表。对应我 2023 的文档结构入 metadata
递归聚类 + 多层树摘要,检索时兼顾细节叶子与高层抽象。结构化、层级化检索,同样是对纯 flat embedding 的超越。
给每个 chunk 前置文档上下文再嵌入 + BM25,弥补孤立切块丢失的语境。对应我的 chunk metadata 增强
BM25 关键词 + 向量语义并行召回,RRF 融合排序。正是我 V2 的 Keyword + ANN + Metastream 三路召回
chunk 富化 title / entity / keyword / 位置等元数据,让检索能过滤、排序、保留 provenance。对应我的 Metaword / Metastream 召回路
引用选择与句子生成交织,逐句归因到支撑段落,而非事后贴粗引用。正是我的 In Page Annotation 句级对齐,早两年。
逐条对上了:PageIndex / RAPTOR 的「结构感知」= 我的文档结构入表征; Anthropic Contextual Retrieval = 我的 chunk metadata 增强; Hybrid Retrieval = 我的三路召回;ReClaim 的句级归因 = 我的 In Page Annotation。

差别只是时间。这些是 2024、2025 的会议与工程博客,而我 2023 就把它们当成一个系统一起做了 —— 不是因为我预见了未来,而是因为做了十年推荐的人,本能地不相信「一个 embedding 解决所有检索」。 召回要多路、特征要工程、漏斗要分层 —— 这些直觉,我早就有了。
2024–25第五章

把等待拆开

Microsoft Edge · Wukong(基于 Browser Use 0.1.17 的工程改造)

2024 年 12 月 18 日,开源项目 Browser Use 还停在 0.1.17。第二天,Wukong 的脚手架进了微软仓库。 我们没有从头再造一个会点击网页的 demo,而是拿那份代码做底座,追一个更难也更实际的目标: 让浏览器 Agent 进入 Edge 的前后端体系,而且让用户不必等它想完才看到它动。

当时开源版的一步很干净,也很串行:

Browser Use 0.1.17 · browser_use/agent/service.py:159–177
state = await browser_context.get_state(...)
model_output = await self.get_next_action(input_messages)
result = await self.controller.multi_act(
    model_output.action, browser_context
)

先采状态,再等模型返回完整结构,最后执行动作。任意一段没结束,下一段都不能开始。 这段代码没有错;它只是把所有延迟排成了一支队伍。

延迟不是一个数字。
它通常是一串还没被拆开的等待。
Edge Client
页面、Tab
执行与呈现
WebSocket RPC
correlation id
双向命令
Perception
DOM、截图
坐标与状态
LLM astream
部分 JSON
持续到达
Action Queue
动作闭合
立即入队
Controller
校验、重绑
执行或刹车
01
先把浏览器从 Agent 进程里搬出去

开源版的 BrowserContext 直接持有 Playwright 的 Page: DOM、截图、点击都在同一个 Python 进程里完成。Wukong 改成 AgentFactory → DriverServer(session) → WukongBrowserContext。 浏览器留在 Edge / Windows 客户端,后端通过统一 Driver 发命令。

Wukong · server/channel.py:113–135 · driver/driver.py:23–29
id = str(uuid.uuid4())
data = {'command': command, 'args': args, 'id': id}
response = await self.send_message_for_result(json.dumps(data), id)

class DriverServer(BaseDriver):
    async def send_command_and_get_response_sync(self, command, args=''):
        return await self.session.send_command_and_get_response_sync(command, args)

CommunicationChannel 管传输,AgentFactorySession 管会话与 Agent 生命周期; 每条命令有自己的 correlation id,每个 session 独占一个 Agent。WebSocket 只是其中一种通道, 不再等于业务本身。

02
截图不是一张图,是一套坐标系

原版直接调用 page.screenshot()。到了真实 Edge 客户端,DOM 框使用 CSS 像素, 截图却可能经过页面缩放和 HiDPI。前端因此只回原始数据,后端负责把它们重新对齐。

Wukong · driver/driver.py:66–87 · browser/context.py:1205–1228
screenshot_b64, zoom_ratio, device_pixel_ratio, highlightInTheFrontEnd = \
    await self.driver.get_screenshot_as_png()

total_zoom_ratio = zoom_ratio * device_pixel_ratio
highlighted_screenshot = self.draw_highlights_on_screenshot(
    screenshot_b64, content.selector_map, total_zoom_ratio
)

draw_highlights_on_screenshot 先检查属性,再检查数值类型,最后把部分越界的矩形裁回画布。 三层失败都选择 continue。感知层可以少画一个框,但不能因为一个坏坐标丢掉整帧证据。 这段后端绘框,以及随后处理 device zoom ratio 和长页面 rect 的修复,在提交历史里都落在我的名字下。

03
动作一闭合,就先让它跑

我先把 action streaming、input streaming 和 writing action 接进主链路; 后来我带团队把这些实验收拢成独立的 WukongEngine。 核心从 await 完整答案 变成 producer–consumer:模型持续吐部分 JSON, ActionProducer 把已经完整的动作压进 asyncio.Queue,Controller 同时消费。

Wukong · agent/wukong_engine/service.py:111–141, 193–225
async for agent_output in chain.astream(input_messages):
    await self.action_producer.process(agent_output)

new_action = self._detect_new_ready_action(agent_output)
if new_action:
    action = self.ActionModel.model_validate(new_action)
    await self.queue.put(action)

# 第 N+1 个数组元素已经出现,第 N 个必然闭合
if next_action_index < len(agent_output['action']) - 1:
    return True

await self.action_producer.add_last_action(agent_output)

最巧的地方只有一个小于号。它不去数 JSON 括号,也不维护另一套 parser; 它利用数组的不变式判断动作何时完整。最后一个动作没有“下一个元素”替它作证, 所以等流结束后再补一次。

输入框又多一层:首段仍是 input_text,负责 focus;后续 token 被改写成模型看不见的 writing 影子动作,只发送新增字符。如果模型回头改写前文, startswith(cache) 失败,就放弃增量,整段重发。

04
流式敢上线,靠的不是快,是会刹车

模型生成动作时看到的是旧页面,执行时页面可能已经变了。开源版当时只在后续动作前比较 branch_path_hash 集合,发现新元素就停止。Wukong 把它拆成三次判断:索引是否存在于缓存、 元素是否仍存在于新页面、页面是否出现了新元素。

Wukong · controller/service.py:98–109, 167–208
new_state = await browser_context.get_state(use_vision=False)
error_msg = self.legility_check(
    action.get_index(), cached_selector_map, new_state
)

if error_msg:
    producer_stop_event.set()   # 执行端反向关停动作生产
    break

action = self.update_action_index(cached_selector_map, action, new_state)
# xpath_hash 相同:元素还在,只是编号漂了,自动重绑

xpath_hash 是元素身份证。能重绑就继续,重绑不了才停。 更重要的是 producer_stop_event:消费者发现证据失效后,producer 会丢弃后续 partial output, 不再把新动作压进队列;底层模型流仍可能自然结束。这条反馈边,才让“边想边干”不等于“边想边闯”。

05
用户看到的速度,也是一条独立事件流

同一份部分输出被投影成两路:Side Pane 保留 eval、memory、next goal 和每个 action; Blue Box 只取最值得说的一句,优先级是 answer、next goal、evaluation。 两路都有自己的 queue,主消息间隔是 0.075s。停止时先排空队列,再 cancel listener, 避免最后一句话丢在路上。

Wukong · server/websocket_channel.py:25–69
# 收:并发处理,不让一个慢消息堵住整条连接
async for message in self.websocket:
    asyncio.create_task(self.handle_message(message))

# 发:必须串行,否则多个流式分片会交织
async with self._send_lock:
    await self.websocket.send(message)

这里的非对称是故意的:收可以并发,发必须有序。 _send_lock 是我后来补上的一把小锁。没有它,前面做得再细的 streaming, 到最后一米也会乱序。

另一条早期线:工具不必全进 Prompt

Wukong 还有一个只有 29 行的 ActionRetriever:默认动作一直保留,自定义动作先写入 FAISS, 每次只按任务取 top-k,再拼进 Prompt。动作池可以继续长,进入 context 的成本保持在 O(k)。 这已经有了后来“记忆与工具按需装配”的影子。

已有能力,也要说清楚:Replay

轨迹回放不是 Wukong 从零发明的。Browser Use 0.1.17 已经有 rerun_history、重试和历史元素重新定位。 Wukong 做的是产品化:把 rerun_history 变成 session action,补上历史列表与 HTTP 入口, 让回放过程继续走实时日志,并能把成功轨迹交给 WorkflowGenerator 保存成可复用 workflow。 这类边界值得说清楚。改造一个开源系统的价值,不靠把上游已有能力算到自己头上。

把今天的 Browser Use 当作后视镜

我又把同一个开源项目今天的 0.13.8 拉下来逐层对了一遍。它已经不是 2024 年那套小循环: BrowserSession 里有共享的 Bubus EventBus,DOM、截图、下载、弹窗、安全分别由 Watchdog 订阅; DOMWatchdog 还会并行启动 DOM build 和 clean screenshot 两个 task。 浏览器内部事件化、感知任务并行化,后来都成了正式架构。仓库也出现了 Python 后端绘框和 DPR 换算, 但全仓没有调用它的主路径;这一点只能算方向相似,不能倒过来写成同一套实现。

Browser Use 0.1.17 · 2024Microsoft Wukong · 2024–25Browser Use 0.13.8 · 现在
运行边界Playwright Page 与 Agent 同进程Edge Client 执行;后端 Agent + Driver;双向 RPCCDP + BrowserSession + SessionManager
感知DOM 后截图,顺序采集客户端回原始 DOM / screenshot / zoom;后端组装和绘框DOMWatchdog 并行构建 DOM 与 clean screenshot
事件step 内直接调用Queue + Event + correlation id;生成、执行、UI 可互相反馈Bubus EventBus + 多个独立 Watchdog
模型到动作完整返回后执行astream 部分 JSON;动作闭合即入队await llm.ainvoke 完整返回,再 multi_act
页面变更新 branch hash 出现就中断后续动作三判合法性 + xpath hash 重绑 + consumer 反向刹车terminates_sequence 静态标记 + URL / focus runtime guard
高亮坐标页面内 JS / Playwright后端 PIL;zoom × devicePixelRatio;越界裁剪主路径取 clean screenshot,再做 browser-side highlight;另有尚未接入主调用链的 Python DPR 绘框实现

但“都用了事件”不等于同一种 streaming。在我核对的 0.13.8 主 Agent 路径里, 代码仍然先 await self.llm.ainvoke(...),拿到完整 AgentOutput 后才进入 multi_act。 它把浏览器内部彻底事件化了;Wukong 当年更激进的一步,是让模型生成动作和浏览器执行动作在时间上真正重叠

代码锚点。 旧版基线:browser_use/agent/service.pybrowser_use/browser/context.pybrowser_use/controller/service.py; Wukong:agent/wukong_engine/service.pyserver/channel.pydriver/driver.pystreamer/service.py; 当前版:browser/session.pybrowser/watchdogs/dom_watchdog.pyscreenshot_watchdog.pyagent/service.py。 对照基于开源旧提交 39aa9e7 与当前提交 85ddbfe

提交历史只记录谁按下了 commit,不能完整表达一个项目是谁带出来的。 我是 Wukong 的团队负责人,整体路线、架构取舍和迭代节奏由我主导;其他成员提交的 PR,也在我的指导下完成。 我从 WSS、接口和第一版 streaming 开始,接着亲自做 server-side screenshot、action / input / writing streaming、 WebSocket 写锁、device zoom ratio 和长页面 rect,再带团队把原型重构成今天看到的 WukongEngine。 属于我的不只是某几行代码,而是让这条链从客户端通信一路穿过感知、生成、执行和呈现, 最后变成一套能跑、能快、也能停下来的系统。

速度不是把 sleep 改小。
是把等待拆成事件,再给事件之间加反馈。
2025–26第六章

真正的 Production,
先决定要不要点

Microsoft Copilot · Contextual Chat × Browser Automation × CUA V2

Wukong 解决的是怎样让浏览器 Agent 更快、更稳地行动。进入 production 以后,我设计并主导了两次关键接入: 把 Contextual Chat 纳入微软共享的 Ensemble Orchestrator;再把 Browser Automation 封装成 BAT Tool, 接进 Orchestrator 的工具体系。

这不是给“总结页面”和“填写表格”各写一条固定分支。经过多轮 Prompt 实验、Tool Review 和架构设计, 主聊天既能获得当前页面与 Tabs 的上下文,也能在工具可用时把任务委派给 BAT 子代理。 模型再结合会话模式、客户端能力和任务本身,判断此刻应该继续读取,还是进入浏览器操作。 因此在 Browser Action mode 中,即使只是读取页面,也可能被委派给 browser_automation

6.1 · 让一套编排既能理解页面,也能委派行动
Read · 理解页面
Contextual Chat
把 Contextual Chat 纳入共享 Ensemble。调用方能够提供 Edge context 时,pre-turn 插件注入当前页和 Tabs 的轻量元数据;Prompt 再引导模型判断是否读取正文。
Act · 改变页面
Browser Automation · BAT Tool
把 Browser Automation 封装为 Orchestrator 可调用的工具。当工具选择阶段提供这项能力时,外层模型通过 browser_automation 委派点击、输入和多步网站任务。

Contextual Chat 并没有另起一个名为 ContextualChatOrchestrator 的大系统。 它是在共享聊天编排上叠加 Edge 上下文:可解析的本轮 actionContext 优先; 只有启用多 Tab 路径、并已把页面上下文写入 conversation cache 时,缺失的本轮上下文才可能回退到缓存。 模型先看到标题、URL 和 tab id;是否读取正文由 Prompt 引导模型选择,并非代码自动路由。

Two routes · outer chat and isolated tool conversation
常规 Contextual Chat 页面理解
Edge request → pre-turn context → model-selected edge_get_page_content → answer + citations

Browser Automation 被选中并由模型委派
Ensemble → browser_automation → isolated tool conversation → summary / consent

这条分界让“浏览器里发生的事”不再等于“都必须由同一种方式完成”。 常规页面理解可以留在主聊天里;需要交互与持续操作时,再委派给隔离的工具会话。 最终选择仍受会话模式、功能开关、客户端能力和模型判断共同影响。

6.2 · BAT 不只是工具,工具里面还有一个 Agent

对微软共享的 Orchestrator 来说,BAT 是一个可选择、可审查的工具入口;进入这个工具以后, Browser Automation 又是一条独立的编排边界,但不是一套独立部署的微服务。 它以 Tool-as-an-Agent 的方式编译进 Chat 服务:外层 Ensemble 发出任务, 按名称隔离的子编排器(keyed orchestrator)在同一进程中启动自己的模型、Prompt、工具与循环,做完后只把摘要交还给外层。

Ensemble
理解请求
选择是否委派
browser_automation
任务、起始 Tabs
前台或后台
Keyed Orchestrator
独立上下文
独立编排边界
Charapita
专属 Prompt
选择下一步
Computer Tools
DOM、视觉
Tabs、历史、搜索
Summary
停止操作
把结果交回外层

子代理每一轮先执行上一轮工具调用,把结果放回 conversation,再渲染 Prompt、调用模型,决定继续还是结束。 当前代码快照把循环上限设为 50;Charapita 路径结束后会清空工具,再额外做一次只负责总结的推理。 遇到需要用户确认的高风险动作,循环会提前停下,保存状态,等用户允许后再继续。

6.3 · DOM 是主路,视觉是退路

本地代码快照中的 Charapita Prompt 没有在“纯视觉”和“纯 DOM”之间选边站。 Prompt 明确要求优先使用 DOM 工具,并称其显著更高效;任务本身依赖视觉或 DOM 信息不足时,再使用截图与坐标工具。 Google Docs、Sheets、Slides 这类页面,是 Prompt 明列的视觉退路示例。这是模型策略,不是代码自动执行的 DOM / 视觉路由。

默认路径
读取 DOMcomputer.get_dom 返回带 node id 的页面结构。
按节点操作computer.dom_do 是 Prompt 要求优先采用的高效路径。
视觉退路
查看屏幕computer.get 获取页面视觉状态。
按坐标操作computer.do 处理 Canvas、复杂布局和纯视觉判断。
失败恢复
长动作链失败缩短为 1–3 步,重新读取 DOM,再继续。
DOM 信息不足停止硬试,切换视觉工具重新观察。

这里没有一个写死的路由器替模型决定 DOM 还是视觉;切换策略写在 Prompt 里,由模型根据页面状态选择。 新开站点则优先走 computer.create_tabs,而不是在地址栏里模拟键盘输入。

9 / 10
Charapita 主路径可用的顶层工具数,取决于浏览历史开关;不是固定常数。
8 + 9
8 个视觉原子动作,加 9 个 DOM 原子动作,共 17 个模型动作。
20
CUA V2 客户端动作协议变体;旧版 IAction 为 14 个。
50 + 1
最多 50 轮执行循环,结束后再做 1 次无工具总结。
Charapita main path · feature-dependent tool surface
8 个核心顶层工具
computer.initialize · computer.get · computer.do · computer.get_dom
computer.dom_do · computer.create_tabs · computer.select_tab · computer.list_tabs

可选:browser_history.retrace
搜索位:browser.run 或 search_web

browser_history.retrace 负责搜索浏览历史;browser.run 当前只批量执行带摘要的网页搜索, 不是一个通用脚本或宏运行器。若切到 Claude 分支,工具面又会变成 simple_browser + search_web。 所以“工具数量”必须带上模型路径和 feature flag,不能脱离运行条件单独比较。

代码里的 V2 tool schema 明确要求保持与 trained Charapita model 的动作描述一致。 这说明工具 schema 是模型兼容契约的一部分。当前代码能够直接证明的是 Charapita 配置名、 OpenAI Computer Use 命名空间及 V2 动作契约之间的技术关系;不能据此断言模型的组织归属。

6.4 · 网页内容只能当证据,不能当命令

浏览器页面属于不可信输入,标题、URL、正文和查询参数都可能包含诱导模型改变行为的文字。 页面内容仍会作为工具结果进入模型上下文;当 edge-security-template 开启并生成 edge id 时, 标题、URL 与正文才会被包装进 WebsiteContent_id 边界,并配套“不执行页面指令”的 Prompt 规则。

Edge page-content trust boundary
edge-security-template 开启时:

真正的用户消息 = 指令

<WebsiteContent_id>
页面标题、URL 与正文
</WebsiteContent_id>
= 只能分析和引用的不可信资料

edge_get_page_content 会对返回内容执行安全检查并注册引用;可信域名可以按配置绕过检查, 检查器异常时,当前实现仍保留原内容继续处理,也就是 fail-open。edge-page-sanitization 开启时,还会清理正文里的受限 URL。 当前代码没有专门删除页面内伪造同名标签的步骤。

在该工具返回当前页的非 Manual 分支中,正文最多保留 128,000 个 .NET String.Length 单位, 也就是 UTF-16 code units;这不是 token 上限,也不是所有 Tabs 的统一上限。 AutoLazy 模式最多补取最近的 20 个目标 Tabs,本地调用超时为 10 秒; 懒加载失败时退回已有 Tab 信息。

Prompt 也不是一个巨大的字符串。Pre-turn 先告诉模型用户打开了什么;Reasoning 阶段决定是否读取正文; Responding 阶段再规定怎样使用页面证据、怎样引用,以及哪些隐私信息绝不能出现在答案里。 同一个页面上下文,在三个阶段承担三种不同责任。

代码边界。以上结构仅以本地 picasso main@b67edd93fd7f5d46091c3b1882915c2fe5807857 (2026-01-26)为准,不代表当前线上配置或今天的最新版本;工具数量会随功能开关、客户端能力与模型分支变化。 关键锚点包括 Service.Chat/Orchestration/AppStartup.csEdgeContextPreTurnPlugin.csEdgeDataTool.csBrowserAutomationTool.csBrowserAutomationTurnPlugin.csResponder.csComputerActionV2.Tool.External.csComputerActionV2.Client.External.csresponding-harmony-charapita/prompt.liquid

这套 production 分层由我设计并主导。Wukong 的事件化经验在这里延伸为“理解页面”和“操作页面”两类能力路径; 上下文、工具循环、授权与安全机制由团队在这套架构下实现并持续迭代。 它不再只是一个会点网页的 Agent,而是一套能够读取、行动,并在需要时停下来请求确认的系统。

真正成熟的 Browser Agent,不是什么都去点。
它先知道什么时候只需理解,什么时候才值得行动。
产品主线第七章

从一组 Tab,
到一条 Workstream

Microsoft Edge · Tab Group → Journey → Intent → Remix

Tab Group 最早处理的是空间上的散乱:同一时间打开的许多页面,怎样被组织成一组。 但真正留下来的不是“分组”这个功能,而是它背后的方法:先汇聚,再凝练。 页面是散的,用户正在做的事却只有一件。

7.1 · 先让模型学会整理 Tabs

按当年的项目记录,最早的大模型方案主要靠 Prompt。2023 年那批 Prompt 里已经有分步骤思考; Tabs 太多、一次放不下时,就拆成几批分别处理,再把结果合起来。当前可检查的产品代码也保留了超长 Tabs 的拆分路径, 但它只能旁证这个方向仍在延续,不能单独证明 2023 年使用的是同一段实现。

我们还花了相当一段时间做自己的小模型。路线很直接:把真实 Tab Session 里的 URL 和 Title 组成输入, 把 chosen 中的分组作为训练目标,用 LLaMA-Factory 做监督微调,再用 LoRA 低成本更新训练模型。 按当年的项目记录,这些 LoRA 实验对应 Nezha 的不同版本。

Tab Groups · LLaMA-Factory + SFT / LoRA
Tabs 的 URL、Title
→ 组成分组任务
→ chosen 作为目标输出
→ prompt token 的 label 被 mask
→ 在 prompt 条件下对 chosen token 计算 loss
→ LoRA 更新模型

训练文件里虽然同时有 promptchosenrejected, 但普通监督微调只使用 promptchosenrejected 不参与。 Prompt 仍作为条件上下文进入模型,只是它的 token label 会被设为 -100,不直接计入训练损失; 普通监督微调优化的是 P(chosen | prompt)

这在当时是很务实的路线。真实 Session 能带来复杂、多语言的浏览场景;LoRA 让实验成本可控; 相比只用相似度做聚类,小模型可以同时决定哪些页面该放在一起,以及这一组应该叫什么。 它先回答了一个很重要的问题:模型能不能完成 Tab Group 这件事?答案是能。

7.2 · 模型学得很认真,数据却没有统一标注口径

很多年后重新检查那批训练数据,我才更清楚地看到其中的数据质量风险。监督微调不会替我们判断答案是否合理; 它只会忠实学习我们给出的答案。如果同一组页面今天按网站类型分,明天按内容主题分,后天又按用户任务分, 模型面对的就不是一道难题,而是三套互相打架的规则。

重复与冲突
重复样本被反复加权10,642 行 Prompt 重复,其中 9,311 行连答案也完全相同。
同一个输入出现多个答案1,134 组相同 Prompt 对应不同 chosen,模型无法从相同 Prompt 中判断应该遵循哪一种分组口径。
规则自己打架
指令要求每组至少两个 Tabs但 106,814 个分组里有 54,316 个单页组(singleton),占约 50.9%。
格式错误也会被学进去1,609 条存在 Tab ID 遗漏或额外分配,558 条把同一个 ID 重复分配。
标签没有同一层级
场景、产品、任务和内容类型混在一起Work、Excel、IT Support、Documents、具体主题都可能成为组名。
Work 覆盖范围过宽它出现 1,154 次,却覆盖企业系统、邮件、学校、培训、文档和 Dashboard。这与后来观察到的 Work 误触发现象一致,但仅凭统计不能确认因果。
长输入与无效反例
最长样本约 81K 字符如果训练时只保留 1K 或 2K token,后面的 Tabs 和答案都可能被截掉。
34,749 条 rejected 完全相同全是 “Sorry, I don't know.”;监督微调本来就不用它,若直接用于偏好训练,也只能说明 chosen 优于拒答,不能教模型区分具体的错误分组。

当时我已经在追这些问题:绝大多数分组到底有多大(Group Size P90 / P95)、Work 在训练集里的分布、Work 和 Health 会不会互相干扰、 输入顺序有没有影响、是否应该移除那些特别常见的组名。今天看,需要排查的问题不只是 Work 的频次, 还包括宽泛标签、粒度混乱、重复和冲突答案、大量单页组、页面类型偏置,以及缺少能区分具体错误的反例。 这些统计能证明频率、冲突和不一致;它们是否以及在多大程度上影响模型效果,还要通过对照实验验证。

训练框架完成了它该做的事。
但如果“好分组”的标注口径不一致、也没有显式说明,模型学得再认真,也只会把这些冲突一起学进去。

如果今天重做,我会先去重、隔离冲突答案、统一标签层级,修好 Tab ID 和单页组,再按真实任务划分训练集与测试集。 先用干净的 prompt → chosen 做监督微调,再收集模型在真实样本上产生的错误分组, 构造成有信息量的 chosen / rejected 对,再在明确的场景与标注准则下做偏好训练(DPO), 让模型学习何时应使用更具体的名称、何时应按用户任务而非页面类型分组,并减少 Tab 遗漏、重复分配和不完整分组。

来源边界。当前产品代码用于核对 Nezha 调用与超长 Tabs 拆分路径; 当年的训练文件用于核对 LLaMA-Factory、SFT / LoRA 及数据统计。 历史 checkpoint 身份与项目时间线来自当事人的项目记录,当前 checkout 不用于反推这段历史。
7.3 · 两页 Slide,一段美好的记忆

我一直很喜欢这两页。第一页是过程,第二页是被看见的时刻。它没有把项目写成一路顺风: 2023 年 10 月那次 DV3 尝试,结果就是 flat experiment。但时间线没有停在那里, 后面还有 streaming model、GPT4-Turbo,以及为 Nezha 和单页组改进做上线准备。

Tab Auto Grouping Our Story:2023 到 2024 的发布、实验与产品指标时间线 点击放大 ⤢
2023–2024 · Our Story
不是每次实验都有提升,但项目一直往前走
从英语范围上线到全球发布,从一次没有带来提升的 DV3 实验,到 streaming model 与 GPT4-Turbo。团队持续观察接受率、Auto Grouping DAU 和自动创建的 Tab Group DAU;2024 年 7 月,Nezha 与单页组改进已进入发布准备。
Tab Auto Grouping Brand:官方传播、外部讨论、Satya 公开引文卡与公开舞台画面;内部邮件截图已替换 点击放大 ⤢
2024 · Brand
后来,它不再只是项目组里的一个功能
它进入 Edge 官方介绍和外部讨论。Satya 邮件中可清晰辨认的原文是 Love the "group my tabs" feature...;公开舞台字幕完整句为 In fact, my most favorite agentic experience today is organize my tabs using Edge Copilot. 公开展示版已将内部邮件截图替换为引文卡。

第一页记录工程怎么熬过一次次版本,第二页记录一项功能怎样进入产品和品牌的叙事。 这份认可属于功能和整个团队。对我来说,最珍贵的是另一件事:我们从“怎样整理一堆散乱的 Tabs”出发, 最后真的让人感到,浏览器开始替自己理解和组织工作了。

这条线继续向前,变成了 Journey。Journey 不只是多收几个 Tab, 它把用户进入 Copilot 之前的浏览轨迹,与进入 Copilot 之后的交互接到同一段连续上下文中。 用户之前看过什么、现在停在哪里、接下来可能需要什么,开始被放在一起理解。

这是我做 Journey 时真正想解决的问题:不是给历史记录换一种排版,而是从一堆浏览证据里, 判断用户正在推进什么,以及此刻最值得主动帮助什么。 代码最后长成了五层:History Summary、增量上下文、两条演进管线、任务判断,以及最后的 Ranking。

7.4 · 页面先被压成证据

History Summary 的产物并不是一句普通摘要。BrowsingPage.ShortBody 里装的是序列化后的 SummaryCoreData:除了正文摘要,还保留页面类型、主题、紧急信息和可行动的结构化事实。 价格、库存、截止日期、活动时间,这些东西不是为了“摘要看起来更完整”,而是为了让下游能够做判断。

SummaryCoreData · stored as JSON in BrowsingPage.ShortBody
{
  "Summary": "Sony WH-1000XM5 的售价、续航和降噪能力",
  "PageType": ["product_page"],
  "MainTopics": ["headphones", "noise cancellation"],
  "UrgentInfo": null,
  "ExtractedInformation": "Price: $349; Battery life: 30 hours"
}
Evidence
PageBody → History Summary
生成结构化 ShortBody:Summary、PageType、MainTopics、UrgentInfo、ExtractedInformation。
Pipeline A
SUA / Journey
Suggested User Action:识别任务、生成卡片、排序、去重、TTL 与卡片演进。
Pipeline B
Intent Evolution
维护具体 next-step task 的身份、阶段,以及 NEW / ADVANCE / carry-forward。

两条管线对证据的使用也不完全对称。SUA 没有 ShortBody 时,可以从 PageBody 补做 History Summary; Intent Evolution 当前不会补摘要,只使用已经存在的 Summary,再结合 title、URL、访问次数、停留时间等信号。 这不是一句 prompt 能抹平的差异,它直接决定了两条状态线可能看到不同的世界。

7.5 · 增量不是再算一遍,是把新证据接到旧状态上

每次生成 Journey,模型看到的不只有“这次来了哪些页面”。 与增量续接直接相关的旧卡和页面上下文被明确拆成三块: 以前生成过什么卡片、上轮之前的旧页面、上轮之后的新访问。旧卡来自客户端回传的 JourneysHistory,旧页和新页则按最近一次 Journey 的 ActivityTime 切开。

进入 Prompt 的三块增量续接上下文
它回答的问题
PREVIOUS GENERATED SUAS
以前已经建议过什么,哪些卡片不能原样重复
EXISTING BROWSER HISTORY
本次重传、但时间上属于上一轮之前的页面证据
RECENT NEW VISITS
上轮 Journey 之后,真正推动状态变化的新行为

当 existing 和 new 同时存在时,旧证据先拿一半页面 token,剩下的留给新访问。 这不是简单的“最近页面优先”,而是在有限 context 里同时保住连续性和变化量。 用户对 Journey 的点击、关闭等交互事件,还可以另以 USER JOURNEY INTERACTION HISTORY 注入。

卡片本身也会演进。模型用 RelatedToHistoricalSUA 指向旧卡索引; 后端据此淘汰被替代的旧 Journey。也就是说,“新卡是否建立在旧卡之上”由 LLM 做语义判断, C# 负责执行替换。一个看起来很小的索引字段,实际承担了卡片状态迁移。

7.6 · 同样叫演进,演进的是两个不同对象
OBJECT
SUA / Journey演进的是用户最终看到的主动建议卡片。
Intent Evolution演进的是具体、可恢复的 next-step task 身份。
STATE
卡片状态标题、按钮、Prompt、来源页、ActivityStatus、TTL、排序和旧卡替换。
任务状态稳定 Id、Stage、FirstSeenAt、LastAdvancedAt、AdvanceCount。
ACTION
展示层intent-like reasoning 加上 action、presentation、ranking 与有效期。
任务内核一个 Intent 恰好对应一个 action;当前支持 NEW、ADVANCE 与 carry-forward。
工程现状 · 产品方向
当前代码:Evidence → SUA pipeline ↘ Intent pipeline
两条管线并行运行。Intent 不是先生成、再投影成 SUA;Retired intent 也还没有完整闭环。
目标方向:Evidence → Unified Intent State → SUA projection
Intent 是从 Journey 概念里抽出的任务内核,但还没有成为所有主动建议的统一上游状态层。Workstream 要解决的,正是这个统一。
Journey 不是一张历史卡片。
它是一套让建议记得过去、看见变化、知道何时该换挡的系统。
7.7 · 最难的不是生成卡片,是忍住不打扰

这份 Journey Prompt 开头先钉了一句话:Task Detection, NOT Interest Recommendation。 浏览器能看到很多行为,但“看过”不等于“正在做”。只有能推断出明确终点、而且尚未完成的事情, 才有资格成为 Journey。随手读新闻、刷视频、打开两篇同主题文章,不能因为内容相近就被包装成一个任务。

TASK
有可以完成的终点购买、选择、学习并做出东西、为交付物研究、配置环境、申请或预订。
有尚未完成的证据用户仍在比较、细化、跨来源核对,页面里没有确认、收据或提交成功信号。
INTEREST
只有内容消费零散新闻、娱乐、社交媒体和一次性的知识阅读,没有一个需要抵达的结果。
即使跨站也可能不是任务同一突发新闻看了很多篇,仍可能只是在跟进事件;除非出现查证、分析或产出材料的行为。
UNCERTAIN
不强迫行为只有一种解释每个主题簇先保留 2–4 个可能目标,分别写出可能性和证据。
替相反解释辩护Prompt 还要求写出另一种解释,以及为什么“这是一项任务”更可信;论证不成立就主动降级。

Prompt 用六类信号判断这件事:是否跨网站、是否跨时间回来、搜索有没有逐步收窄、投入了多少时间、 离真正行动还有多近,以及目标是否还没完成。随后再过一道很硬的门: taskConfidence ≥ medium,并且最可能的 Intent 也至少是 medium,才允许生成卡片。 两页组成的薄弱主题通常会被挡住;只是在跟进突发新闻的主题簇、纯内容消费和已经完成的事情,则命中明确的阻断规则。

页面没有摘要,也不能成为漏掉任务的理由。只有选中的至少两页来源都带摘要时,Journey 才能进入能读取页面内容的 Contextual Chat;否则要么移除缺少依据的页面并保留足够来源,要么切到只依靠明确任务描述和网页搜索的 Exploratory Chat。 系统承认自己此刻看不到正文,但不会因此把已经识别出的任务扔掉。Prompt 最后还会再检查一遍所有通过门槛的主题,避免漏卡。

主动帮助的第一原则,
是知道什么时候不该出现。
7.8 · 卡片也要知道事情走到了哪一步

Journey 不是一次性推荐。Prompt 会判断任务是刚开始、仍在推进、隔几天后重新回来,还是已经从研究走到决定。 如果出现购买确认、表单提交、预订完成等信号,旧 Journey 就不再生成;如果任务已经演进,下一张卡要接住新的阶段, 不能把用户重新送回起点。连续五天以上没有新活动、又没有完成信号的任务,会被视为已经放下,不再一直追着用户提醒。

任务状态
Journey 应该怎么做
NEW · ONGOING · RESUMED
给出最接近当前目标的下一步,帮助用户把未完成的事情继续推进
EVOLVED
承认任务已经换挡,生成新阶段的行动,而不是重复旧建议
COMPLETED
停止重复生成,不把已经做完的事重新包装成“待办”
DORMANT · ABANDONED
降低时效或停止追问,让过期的意图自然退出

连卡片上的文字也在守这条边界。它要说“怎样帮你把这件事做完”,而不是“我们看见你浏览了什么”; 不罗列网站,不制造被监视感,也不把内部的页面索引和置信度暴露给用户。 按钮必须指向一个具体动作,Chat Prompt 也只能承诺当前能力真正能看到和完成的事。

7.9 · 最后一道,还是推荐系统

通过任务门槛之后,系统才进入排序。LLM 判断紧迫度、可信度、继续推进的可能性、建议的新鲜度和用户价值, 服务端再补上停留时间与重复访问。

JourneysRankingService · normalized weighted score
RankScore =
  0.20 × Urgency
  + 0.10 × Confidence
  + 0.10 × ContinuationLikelihood
  + 0.20 × InsightNovelty
  + 0.20 × UserValue
  + 0.10 × CumDwellTime
  + 0.10 × RepeatVisitCount

这个分数之后还要经过搜索页过滤、语义去重、来源页重叠惩罚和旧卡时效衰减。 依赖同一批页面的卡片会被降权;旧卡也不是一刀切删除,未点击的建议衰减得更慢,保留一点被看见的机会。

做到这里,Journey 已经不是“让 LLM 给几条建议”。它和我以前做推荐系统时一样: 多路信号进来,先判断任务价值,再做状态演进、排序、去重、时效与多样性控制。 过去推荐系统决定什么内容值得出现;到了 Journey,它开始决定什么帮助值得出现。

代码锚点。BrowsingPage.csDetailedPageSummary.csHistoryBodySummaryService.csPromptBuilderHelpers.csJourneysHistorySplitter.csJourneysRankingService.cs; V7 与 Intent Evolution 的双轨细节来自对应实现与 schema 的代码学习笔记; 任务门槛、消费阻断、增量生成和卡片文案边界来自 Edge Journeys 的 Task Detection Prompt。
7.10 · 从当前双轨,到 Workstream
Space
Tab Group
汇聚同时打开的一组页面,给工作划出边界。
Time
Journey
连接进入 Copilot 前的浏览轨迹与进入后的 Copilot 交互。
The target unit of work
Workstream · 目标方向
Journey 进一步凝练出 Intent;目标是让 Intent 与 Action 成为一件持续推进的工作。
Intent
用户现在想完成什么
Action
为了它,下一步做什么
Move forward
Remix
从理解网页走向改写网页,甚至生成一个新的网页。

Workstream 是这条产品演化的目标落点。Group 先演进为 Journey;Journey 再进一步凝练出 Intent。 目标是把 Intent 与 Action 统一为同一个持续推进的工作单元;系统要保存的不再只是页面和轨迹, 而是用户想完成什么,以及接下来该做什么。

前者是 Intent,后者是 Action。按这个方向,一个 Workstream 可以持续积累相互对应的 Intent 和 Action; 每一次行动又会产生新的页面和上下文,推动系统重新理解下一步。 Intent 是 Journey 的进一步凝练,Remix 则把这种理解真正推进行动。

当然,Action 并不是到了 Remix 才突然出现。Journey 阶段已经有了行动的影子: Copilot 不再只是回顾用户去过哪里,也开始思考接下来可以替用户做什么。 到了 Remix,这层“下一步”才从建议和辅助,落到网页本身。

Group 给它边界,Journey 给它时间,
Intent 给它方向,Action 让它继续向前。

我们一路做的其实是同一件事:先把散落的信息汇聚起来,再把它凝练成用户正在推进的工作, 最后让浏览器替用户把这项工作继续做下去。

2026 · 构想第八章

用户真做过的事,
才值得 Agent 学

Suggested Action(SUV)· FARA 合作构想

Journey 里那层行动的影子,后来被 Johnny 沉淀成 SUV(Suggested Action)。 它正好站在 Intent 和 Action 中间:Intent 说明用户想完成什么,Suggested Action 提出下一步怎样推进, 真正的 Action 再把它执行出来。

这层结构还有另一种用途。对用户,它是“下一步建议”;对 Agent 系统,它是一颗来自真实工作的任务种子。 这让 SUV 可以接到微软 FARA 的数据生成体系上,形成一条新的合作思路。

FARA 从网站出发,问这里能生成什么任务。
我们从用户出发,问人真的想在这里完成什么。
任务来源:从“模型猜”到“用户真做过”

FaraGen / FaraGen1.5 解决的是:没有足够人类演示时,怎样规模化生成 Computer Use Agent 的任务与轨迹。 在真实网站任务这条分支里,系统从网站侧出发,让模型探索站点,再提出一个看起来合理的任务。 这条路线容易扩展,但任务真实性仍要经过模型判断。

合作构想拟从另一端切入:real browsing history → Journey / Workstream → Intent → Suggested Action。 任务不是根据一个网站“可能可以做什么”推断出来的,而是从用户真正做过、真正想继续推进的事情里提炼出来的。 一个从站点能力寻找任务,一个从用户工作寻找任务。

三道筛选:什么任务值得交给 GUI Agent
对用户有用吗
FARA 的公开方向强调生成用户真正想完成的任务,但没有把“对用户有多大价值”单独设成一道筛选标准。
我们的扩展先判断任务是否来自真实需求,是否值得用户花时间交给 Agent。
必须用 GUI 吗
FARA 已经防止模型绕过网页如果模型不操作页面,而是直接读取网站背后的接口来完成任务,FARA 会把它判为错误。
我们再往前筛一步先判断这件事是否真的必须看懂界面、点击界面。搜索或写代码就能完成的任务,不应该拿来证明 GUI Agent 的能力。
任务有多难
FARA 已经做了难度分层任务从 L1 到 L6,操作步数逐渐增加;同一道题也会重复运行,观察它到底有多难。
接入真实任务来自 Workstream / SUV 的任务,也可以按实际运行结果分层,再分别用于评测和训练。

这件事可以交给三种更简单的方法先试一遍:

写代码不看页面,直接用代码或公开接口能完成吗?
搜索信息只靠搜索和整理资料能完成吗?
读取网页结构不看屏幕,只读取页面里的文字和控件信息能完成吗?

如果这些方法就能做完,它测的就不是 GUI Agent 的核心能力。 只有必须看懂页面状态、找到正确控件并实际操作的任务,才真正需要 GUI Agent。

评测:不只判对错,还要知道为什么失败

FaraGen1.5 已经会检查三件事:任务是否真的完成、过程是否绕了远路、关键操作前是否正确停下来询问用户。 它也会记录环境故障、模型编造、执行策略、关键操作和工具调用等不同失败原因。 我们希望再把这些原因归到三类:模型本身不会做、运行环境出了问题、题目设计得不合理。 这样算出来的能力分数,才不会把网站打不开也算成模型能力差。

能力分数 · 把环境故障和错误题目从分母里扣掉
能力分数 =
    完成任务数 /(总任务数 - 环境故障 - 错误题目)

FARA 已能识别“操作结果和回答互相矛盾”以及“没有证据却声称成功”。我们还想把 “Agent 自己认为有没有完成”和实际验证结果放在一起看。 最危险的情况是:任务实际失败,但 Agent 声称已经成功。 不过,当前评测表没有统计模型自报成功的次数,所以这里不再给出“过度自信”的具体数字。

当前内部评测 · 82 个任务 · 能力分数与全部任务成功率分开计算
0.413
FARA 9B · 能力分数
验证完成 26 / 82 · 全部任务成功率 31.7%
平均 24.5 步 · 上下文长度 262144 · 按屏幕坐标点击的浏览器环境
≈ 0.54
FARA 27B · 区间 0.49–0.63
验证完成 29–37 / 82 · 全部任务成功率 35–45%
运行 3 次 · 上下文长度 262144 · 按屏幕坐标点击的浏览器环境
≈ 0.59
GPT-5.4 · 区间 0.57–0.63
验证完成 35–38 / 82 · 全部任务成功率 43–46%
平均约 11 步 · 自有浏览器环境

这三列不能只看最大的数字。能力分数会先排除环境故障和错误题目;全部任务成功率的分母则固定是 82。 FARA 通过坐标点击页面,GPT-5.4 使用自己的浏览器运行环境,两者并非完全同条件,只能放在一起观察,不能简单排出高下。

Fara1.5 还专门加入了“向用户提问”和“暂停并记住信息”两种动作。这也在指向同一个瓶颈: 难点已经不只是能不能点准,而是什么时候该继续,什么时候该停,什么时候应该承认自己还没有完成。 对模型是否清楚自己的能力边界进行评测,关注的正是这一层。

从一次评测,到一体化数据飞轮
01
真实任务
History → Journey → Intent → Suggested Action
02
筛选与执行
用户价值、是否必须操作界面、任务难度;再用多种方法实跑
03
归因与修题
结果验证、环境诊断、任务诊断、置信度校准
04
训练再回来
改进任务与轨迹,补模型短板,再回到真实 Workstream
↺ 04 的结果回到 01,形成下一轮任务

FaraGen1.5 已会针对模型缺陷迭代训练数据,公开评测也会定期更新容易过时的任务。 合作构想拟复用并扩展这些机制,把更上游的真实任务来源与更下游的失败归因接入同一流程。 目标是将任务生成、评测、失败分析、任务改进和训练数据生产连接成一个数据飞轮。

FARA 解决怎样规模化造数据。
这套构想继续追问:什么任务值得造,失败究竟发生在哪里。

在这套构想里,Workstream 的意义又向前走了一步。它最初是为了把页面和 Journey 组织成一件持续推进的工作; 再往下,还可以成为 Agent 最真实的任务来源。

公开对照。Fara1.5 与 FaraGen1.5 的环境、执行与评测架构、 正确性、效率、关键操作检查、交互动作与迭代训练方式,来自 Fara-1.5: Scalable Learning Environments for Computer Use Agents。 “从站点侧生成任务”的比较只针对真实网站任务;FaraGen1.5 同时也会使用为训练搭建的模拟网站。 本章其余内容是基于 Workstream、SUV 与现有评测系统提出的合作构想,不表示合作已经落地。
2026第九章

让 Bot 学会等,
也学会闭嘴

Sociemate · 数字员工

前一章追问的是:Agent 什么时候应该继续,什么时候应该停下来问人。 Sociemate 把这个问题从评测题,搬进了每天真实发生的 Teams 对话。

很多 Bot 显得不像人,并不是模型不够聪明,而是它对时间没有感觉。 你还没说完,它就答;你补了一句,它把两句话当成两件事;你已经改主意,旧任务还在继续跑。

2026 年,我们开始把 Sociemate 做成一个真正住在 Teams 里的数字同事。 我先把 Cloud Gateway 和 Teams bot 入口提交进仓库;后来我们把本地 Bridge、会话状态机、主动性、Memory 和 Wiki 一层层接上。 真正让它开始“像人”的,不是加了一段更亲切的 Prompt,而是让系统学会理解一句话发生在什么时刻

像人的不是口吻。
是它知道,你可能还没有说完。
9.1 · 一条消息,不一定就是一项任务

私聊里,第一句话进来后,Sociemate 先等五秒;如果用户继续补充,安静窗口会扩到二十秒。 群聊更复杂:Teams 通常只把 @mention 推给 Bot,所以系统也先等这段自然停顿, 再补取前十分钟的对话,之后每三十秒查看有没有新的跟进。 它判断的对象不再是一条消息,而是一段正在成形的对话。

这段等待不是为了显得慢,而是为了给用户留出说完整件事的时间。 语义分类器主要在自然停顿、最长等待、群聊取得新证据,或者执行中的任务收到补充时重新判断。 如果 Teams 推送来的私聊或 @mention 恰好在判断进行中到达,当前这轮不会被打断,随后会补做有限轮次的判断; 群聊轮询取到的新消息则留在缓冲区,等下一次轮询再判断。两条路都不会漏消息,也不会无限重判。

9.2 · 真正让它像人的,是六只闹钟

把 Controller 摊开看,里面正好有六个会话级计时器。它们不是拿来排日程, 而是在模拟人和人相处时最难写进 Prompt 的东西:什么时候等,什么时候问,什么时候开工,什么时候先听对方说完。

settleTimer
等你说完
先等五秒;你继续补充,就扩到二十秒。安静下来后,再把连续几句话合在一起判断。
maxWaitTimer
三分钟后唤醒判断
默认三分钟到点后;没有任务正在执行时,系统重新判断该执行、追问、报状态还是继续等。若旧任务仍在执行,就等它结束后再处理缓冲区。
dmConfirmTimer
等你最后确认
只有系统已经选择再等等,而私聊五分钟仍没有新消息,才直接处理已有内容,不再问模型。
pollTimer
去群里看一眼
每三十秒去 Teams 捞一次后续消息;有新内容或积压才重新判断,没有就保持安静。
budgetTimer
群聊不能一直悬着
群里安静太久,就按当前阶段确定性地追问、开工或结束,而不是再让模型猜一次。
interruptTimer
你插话,我先听三秒
干活时收到补充,先听完这一小段,再判断是在修改当前任务,还是另开一件事。

这六只闹钟管的是会话;每个正在执行的任务,还有自己更短的合并窗口。 模型处理语义上的不确定,时钟处理时间上的确定。Sociemate 的拟人感,不是人设,是时间感。

9.3 · 消息不是请求,是一个 Loop

普通 Bot 的边界是一条直线:收到一条消息,生成一次回答,然后结束。 Sociemate 把消息层做成了一个循环。分类不是终点,只是这轮对话的岔路口。

BUFFER
先放进同一段会话
新消息先被记录,不会被切成互不相干的一问一答。
TRIGGER
等到正确的时刻
自然停顿、群聊取到新话,或任务结束后仍有新内容。
CLASSIFY
判断现在该做什么
不是只问“怎么回答”,还问“要不要回答”。
从判断分成四种下一步
01
开始做
消费本轮选中的消息开始执行;完成后再检查有没有新的未处理内容。
02
先问清楚
发出问题,但不丢上下文;用户补充后再回到判断。
03
先报进度
发送阶段性状态,同时保留本轮消息,等待后续输入或再次判断。
04
暂时不说
不形成自说自话的空转;保留消息,等待新输入或确定性兜底。
三条不同的回程触发
新消息推送进来群聊轮询取到新内容任务结束,仍有新的未处理消息
↺ 回到会话缓冲区。 任务完成本身不是新输入;系统会按当前阶段决定立即判断、等一个安静窗口,还是等下一次群聊轮询。

私聊不主动轮询:新消息由推送进入;任务结束时若还有新的未处理消息,系统会立即重新判断。 开启群聊轮询时,系统每三十秒回去看一眼;开始执行后还保留约五分钟的闲置窗口, 有新活动就续上,有任务仍在运行就继续等,没有活动也没有任务才收起这轮会话。

人在它工作时仍然可以改变主意。同一件事有了新要求,系统会先把补充合成替代任务, 而不是立刻掐断旧任务;经过很短的保护期和合并窗口后,再决定是否中止。 如果旧任务已经自然结束,替代任务就接着执行;不相关的事则另开处理。 已被中止或替代的任务,最终回复和后续工具外发都会被拦住,避免过期结果突然回来。

9.4 · 云端负责中转,当下判断留在本地
云端中转
认人、送达、短暂缓存
Teams 消息先经过云端,确认它属于谁的数字员工,再送到那个人的电脑。客户端暂时离线时,消息只在内存里短暂停留。
Teams → Cloud Gateway
身份与路由 → 短期离线缓冲
本地结果 → Teams 回帖
本地大脑
理解、记忆、执行
实时对话和个人知识整理留在用户电脑上;需要共享的团队记忆与 Wiki 才按配置同步到云端。主动消息的预算、冷却和去重仍在本地完成。
会话状态 → 任务判断 → 本地 Agent
本地群聊记忆 · 本地整理个人 Wiki
共享记忆 / Wiki 按层同步
主动层 · 按时醒来,但不随便说话
定时同事云端负责按时叫醒任务,本地 Agent 真正执行;只有需要同步结果时才向 Teams 发声。
群聊记忆每次回复先保存原始问答;默认在对话安静二十秒后批量整理,积到十二个回合就立即处理。
长期知识大约每十二小时在本地整理跨群、跨项目的稳定知识;启用云端配置时,个人 Wiki 可同步存储并从云端读取。

所以“云侧无状态”不是完全不留数据:Gateway 会保留送达和回帖所需的短期内存状态,重启后即消失; 长期记忆与判断仍由本地或独立服务负责。

9.5 · 主动,不等于多说话

数字员工不能只等人点名。Sociemate 的定时任务会看群聊、PR、Multica 和团队记忆, 被标记为主动触发的发言,还要再经过一份出站台账(OutboundLedger):同一段时间说了多少次、同一个触发是否刚发生过、内容是不是已经发过。 预算、冷却和去重,决定了“有能力主动”不会变成“总想插话”。

我很喜欢其中一条产品原则:先干活,再说话。 群聊参与不是为了证明 Bot 在线,而是它刚刚替大家查完资料、看完 PR、跑完命令,手里真的有一个结果。 没做成事,沉默就是正常答案。

数字员工真正像同事的那一刻,
不是它更会说话,而是它终于有了时间感、连续性和分寸。
代码边界。本章按 Sociemate main@d6d86a0 核对。关键路径包括 SociemateAgent.csMessageDelivery.cssession-controller.mjsarbiter.mjsoutbound-ledger.mjsmemory/subsystem.mjswiki/subsystem.mjs。 我直接提交了最早的 Cloud Gateway 与 Teams bot 入口;本地 Bridge、会话化交互、主动性、Memory 与 Wiki 是团队后续共同推进的结果。
2026第十章

把经验装进系统,
而不是塞进一个 Prompt

Societas · Agent Harness × Skills × PPT

Sociemate 追问的是:一个数字同事什么时候该说话,什么时候该继续等。 到了 Societas,我面对的是另一种时间问题:一项复杂工作经过几十次判断和工具操作以后, 怎样还记得自己走到了哪里。

PPT 把这个问题暴露得很彻底。做一份二十页、可编辑、还能继续修改的演示文稿, 不是模型一次写出二十段文字就结束了。它要理解需求、组织故事;启用可编辑 Planner 时,还要等用户确认大纲; 再分批生成、分批审查、转换格式,最后把整套文件交出去。 真正的难点,不是某一页能不能生成,而是整条执行链会不会失忆。

模型给出下一步判断。
系统保证这一步真的接得上上一步。
10.1 · Agent 不是一轮回答,是一条执行链

在 Societas 里,一段持续对话由 thread_id 串起来;一次后台执行有自己的 agent_run_id;同一次执行里,Agent 可以经历一次或多次循环(iteration)。 这三个层次分开以后,长任务才有了可以检查的状态,而不只是聊天窗口里一串看似连贯的文字。

READ
先把状态读回来
对话和上一轮工具结果挂在同一条 Thread 上,成为下一次判断的上下文。
DECIDE
模型只决定下一步
回答、调用工具、继续等待,或正式完成这项任务。
EXECUTE
工具把动作落下去
执行结果被保存,成为下一次判断真正可见的新状态。
后端工具返回结果且当前 Run 继续时,进入下一次 iteration
↺ 回到 Thread。 后端 Tool Result 不是旁路日志,而是下一轮推理的一部分。

当相关功能开关(Feature Flag)开启、Planner 等工具被注册为需要前端参与的 Client Tool 时, 当前 Run 会暂停;前端回传结果后,再启动一个新的 Run 继续。 所以同一项用户任务既可能在一个 Run 内循环很多次,也可能跨越多个 Run。 文件则由 Thread 对应的 Project / Sandbox 工作区承载,不属于 Thread 消息本身。

这也是我后来越来越在意的一条边界:模型适合处理语义上的不确定, 但“工具到底执行过没有”“文件现在叫什么”“上一轮结果是什么”,不能靠模型回忆。 它们必须由系统保存,并在后续判断时重新提供给模型;上下文过长时,还可能经过摘要或裁剪。

10.2 · Skill 不是说明书,是按需展开的工作记忆

如果把 PPT、Excel、搜索、文件操作和各种例外一次性写进 System Prompt, 规则越多,真正重要的规则反而越容易被淹没。Societas 采用了三级渐进加载: 模型先知道有哪些能力,选中以后再读完整方法,遇到具体场景才继续打开参考文件。

LEVEL 01 · METADATA
先知道有什么能力
初始上下文只放 Skill 名称和适用场景。它负责发现,不负责把所有细节一次讲完。
LEVEL 02 · SKILL
选中以后,再读完整方法
模型调用 load-skill,下一轮才拿到完整工作流、工具协议和质量要求。
LEVEL 03 · REFERENCE
只在需要时打开细则
例如在当前对话里,Agent 已加载 PPT 能力并正式交付过整套 PPT 后,后续增删、换序和改页才加载专用规则。

这套结构对我最重要的意义,不只是少放一些文字,而是让经验有了层次。 System Prompt 负责共同边界,Skill 负责专业工作流,Reference 负责少数具体场景。 模型不必在每一轮都同时背着所有规则。

10.3 · PPT 是一条生产线,不是一个生成按钮

首次生成 PPT 的典型流程中,Planner 会先给出标题、页数、主题和每页意图;启用前端可编辑确认时,用户确认后的版本才是后续生成的依据。 页面通常以每批不超过三页的小批次生成 HTML,再逐批做内容审查和修复。

01
规划与确认
启用可编辑 Planner 时,它先把故事线、主题和每页目的摊开;以用户确认后的大纲为准。
02
小批次生成
通常每批不超过三页,每一页是独立的 1280 × 720 HTML。
03
审查与修复
逐批检查内容是否完整,再针对真正改变的页面做必要修复。
04
转换与交付
HTML 转为可编辑 PPTX;当前对话已加载 PPT 能力并正式交付整套 PPT 后,后续请求才进入专用流程。

HTML 是主要的可编辑源。转换器用浏览器真实渲染页面,再把文字、图片、表格和图表尽量还原成 PowerPoint 原生对象;只有难以表达的复杂元素才退回截图。 所以“看起来像一页 PPT”和“交付一页还能继续编辑的 PPT”,是两种完全不同的工程问题。

10.4 · Harness 不是模型外面的一层壳
模型负责判断
理解任务,选择下一步
读懂用户意图,选择 Skill 和工具,组织内容,判断何时需要继续、澄清或完成。
需求理解 → 专业方法
内容生成 → 下一步决策
异常解释 → 用户沟通
代码负责确定性
保存状态,把动作做实
执行工具、保存结果、维护 Thread、同步文件,并把 HTML 稳定转换成最终交付物。
Tool Call → Tool Result
文件状态 → 格式转换
运行状态 → 完整交付
当前机制反过来给我的提醒 · 不该继续让 Prompt 承担的事
页面结构插入、删除、换序、防覆盖重命名和连续编号,本质上都是确定性状态变换。
质量边界只检查新建或内容真正变化的页面,不该让多处自然语言规则互相打架。
交付完整性旧合并文件是否失效、附件有没有漏页,应该由统一工具核对,而不是靠模型记住。

代码里仍有不少规则靠 CRITICALMANDATORYNO EXCEPTIONS 反复强调。但当每一条都说自己最重要,优先级就等于没有优先级。 更稳的方向,是把插页、删页、换序和交付核对收进确定性工具, 让工具直接返回“哪些页面只改了编号、哪些页面真的改了内容、接下来该检查什么”。

把需要判断的留给模型。
把不该猜的写成代码。

回头看,Societas 让我更清楚地理解了 Harness Engineering: 它不是在模型外面包一层调用代码,而是在设计模型能看见什么、什么时候看见、 每一步如何留下证据,以及哪些错误根本不应该交给模型避免。 Skill 把经验变成可复用的方法,Agent loop 让方法能够继续执行,确定性工具则守住交付的底线。

机制边界。本章依据 2026 年 9 月 1 日对 Societas 本地代码快照的逐段梳理。 核对范围包括一次 Agent 执行的生命周期、对话线程与工具结果如何进入后续判断、 两种工具调用协议(文本 XML 与模型原生函数调用)、Skill 渐进加载、PPT 首次生成、 交付后的后续操作及 HTML → PPTX 转换。 文中关于“应下沉为确定性工具”的部分是基于现状得出的工程判断,不表示这些改造已经完成。