这不是一份简历,是一条主线。
2023 年,大模型刚起来,我开始做浏览器里的大模型应用 —— Contextual Chat。 那会儿 ReAct 的论文才刚出,业界还没有「Agent」的范式共识,更没有现成的框架可抄。 但当我把这套系统的架构画出来时,它已经天然长成了后来所有 Agent 都在用的样子。
看那张 2023 的 Overall Architecture:
用户的自然语言先进 Triggering (Intent Understanding) 做意图理解,
分流成 Non-Contextual 和 Contextual 两条路 —— 这就是后来的 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) 生成 —— 这就是 执行。
两年后的 2025,同一套思想被放大成商用、多标签页的 Copilot。
骨架没变,只是每个器官都长大了 —— 从单页 grounding 变成
multi-tab content and metadata,从一条固定 pipeline 变成可编排的
Sydney Flux Orchestrator。
点击放大 ⤢
点击放大 ⤢
这就是我想让你先看到的那个点:我不是追着范式跑,是提前站在了范式将要长出来的地方。 差别只是 2023 还是单页面,2025 变成了 for commercial、for multi-tab。
第一章那张图里,最上面那个框叫 Triggering (Intent Understanding)。
整个系统能不能用,就看它能不能准确读懂用户当下想干什么 ——
是要总结这一页、翻译这一段,还是问这张表里的某个数。
这一章讲的,就是这个「意图」是怎么一步步被我榨出来的。
它不是一开始就设计对的,而是被算力逼着,长出了一套越用越聪明的东西。
最开始简单粗暴:一个 query 进来,直接用 GPT-3.5 解析出结构化意图 ——
Contextual / Non-contextual × Summary / Translate / Rewrite / Question ×
Page / Section / Table / Figure × Slot。
「总结这一页的第三节」→ Contextual · Summary · Section · 3。干净、直接。
但每一个 query 都要打一次 LLM —— capacity 根本扛不住。
算力不够,就换个打法。先离线用 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 它。
开源 embedding 不够好,就自己练了一个 SLM(基于 Mistral SFT)。 这跟同期 Tabs 那条「随效果 / 延迟 / 成本三角持续演进、最后走到自研小模型」的路线是同一个手感 —— 能用大模型的地方先用,扛不住了就往下沉。
再后来,这些能力进入了 agentic loop:模型接手一个任务后, 根据需要调用工具,一路推进到结束。但系统入口仍有另一个问题——并不是每个 question, 都值得交给同样昂贵的模型。于是上线了我写的论文 WebRouter: 用变分信息瓶颈(VIB)做 cost-sensitive 的模型路由,在后续推理与执行开始前,先按 question 选择模型。
两者不是替代关系,而是可以相加的两层:WebRouter 回答“先由哪个模型接手”, agentic loop 回答“接手以后,怎样把任务做到底”。 不过边界仍要说清楚:WebRouter 做的是 question-level model routing, 并不是前面那套完整的 intent 系统。
点击放大 ⤢
点击放大 ⤢
2023 年做侧边栏长文总结时,我很快撞到一个问题: 「检索」根本不是一件事,而是一堆不同的事。 用户说「总结这篇 PDF」和「这篇里讲 GPT-4 的部分说了啥」和「把电影演员列出来」—— 嘴上都是问一篇文档,实际要的东西天差地别。 用同一套检索去应付所有问法,一定顾此失彼。
所以我把它拆成了两步:先判断这个问题到底是哪一类(sub-intent),再按类别选检索路径。 统计真实用户问题,落成四类:
然后每一类走它自己的路:sub-intent 用 embedding + ANN 或 explicit question understanding 判定,
再分别映射到 sequential part-by-part / page structure-based indexing /
semantic based page indexing 这几条不同的检索路径上。
同一个「找」,被拆成了一张按意图分流的多路径网。
点击放大 ⤢
点击放大 ⤢
今天回头看,这套「先分解问题、再按子问题选检索策略」的思路, 和现在大家说的 search agent 是同一个骨架。下面这段是我让 agent 现查的、 当前主流 search agent 的公开技术 —— 摆在一起看,2023 的这张图站在什么位置就很清楚了。
2023 年做长文档总结,最容易的做法是:切块、embedding、向量检索、丢给大模型。 很多人到今天还停在这一步。但我当时很快就发现 —— 光靠 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 特征工程、粗排到精排的漏斗。 只不过这一次,漏斗最末端接的不是一个排序模型,而是一个大模型。 十年的推荐直觉,在这里直接复用了。
点击放大 ⤢
点击放大 ⤢
光找到、生成还不够。大模型说的每一句,得能指回网页里的原文 ——
这就是 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%。不是感觉变好了,是量出来的。
点击放大 ⤢
点击放大 ⤢
最让我感慨的是:这几年一篇篇会议论文、一个个开源项目, 走的正是我 2023 走过的这条路 —— 大家后来才发现「光靠 embedding 不够」, 要结构感知、要 metadata、要混合召回、要句级引用。摆在一起看:
2024 年 12 月 18 日,开源项目 Browser Use
还停在 0.1.17。第二天,Wukong 的脚手架进了微软仓库。
我们没有从头再造一个会点击网页的 demo,而是拿那份代码做底座,追一个更难也更实际的目标:
让浏览器 Agent 进入 Edge 的前后端体系,而且让用户不必等它想完才看到它动。
当时开源版的一步很干净,也很串行:
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
)
先采状态,再等模型返回完整结构,最后执行动作。任意一段没结束,下一段都不能开始。 这段代码没有错;它只是把所有延迟排成了一支队伍。
开源版的 BrowserContext 直接持有 Playwright 的 Page:
DOM、截图、点击都在同一个 Python 进程里完成。Wukong 改成
AgentFactory → DriverServer(session) → WukongBrowserContext。
浏览器留在 Edge / Windows 客户端,后端通过统一 Driver 发命令。
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 只是其中一种通道,
不再等于业务本身。
原版直接调用 page.screenshot()。到了真实 Edge 客户端,DOM 框使用 CSS 像素,
截图却可能经过页面缩放和 HiDPI。前端因此只回原始数据,后端负责把它们重新对齐。
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 的修复,在提交历史里都落在我的名字下。
我先把 action streaming、input streaming 和 writing action 接进主链路;
后来我带团队把这些实验收拢成独立的 WukongEngine。
核心从 await 完整答案 变成 producer–consumer:模型持续吐部分 JSON,
ActionProducer 把已经完整的动作压进 asyncio.Queue,Controller 同时消费。
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) 失败,就放弃增量,整段重发。
模型生成动作时看到的是旧页面,执行时页面可能已经变了。开源版当时只在后续动作前比较
branch_path_hash 集合,发现新元素就停止。Wukong 把它拆成三次判断:索引是否存在于缓存、
元素是否仍存在于新页面、页面是否出现了新元素。
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,
不再把新动作压进队列;底层模型流仍可能自然结束。这条反馈边,才让“边想边干”不等于“边想边闯”。
同一份部分输出被投影成两路:Side Pane 保留 eval、memory、next goal 和每个 action;
Blue Box 只取最值得说的一句,优先级是 answer、next goal、evaluation。
两路都有自己的 queue,主消息间隔是 0.075s。停止时先排空队列,再 cancel listener,
避免最后一句话丢在路上。
# 收:并发处理,不让一个慢消息堵住整条连接
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,
到最后一米也会乱序。
Wukong 还有一个只有 29 行的 ActionRetriever:默认动作一直保留,自定义动作先写入 FAISS,
每次只按任务取 top-k,再拼进 Prompt。动作池可以继续长,进入 context 的成本保持在 O(k)。
这已经有了后来“记忆与工具按需装配”的影子。
轨迹回放不是 Wukong 从零发明的。Browser Use 0.1.17 已经有 rerun_history、重试和历史元素重新定位。
Wukong 做的是产品化:把 rerun_history 变成 session action,补上历史列表与 HTTP 入口,
让回放过程继续走实时日志,并能把成功轨迹交给 WorkflowGenerator 保存成可复用 workflow。
这类边界值得说清楚。改造一个开源系统的价值,不靠把上游已有能力算到自己头上。
我又把同一个开源项目今天的 0.13.8 拉下来逐层对了一遍。它已经不是 2024 年那套小循环:
BrowserSession 里有共享的 Bubus EventBus,DOM、截图、下载、弹窗、安全分别由 Watchdog 订阅;
DOMWatchdog 还会并行启动 DOM build 和 clean screenshot 两个 task。
浏览器内部事件化、感知任务并行化,后来都成了正式架构。仓库也出现了 Python 后端绘框和 DPR 换算,
但全仓没有调用它的主路径;这一点只能算方向相似,不能倒过来写成同一套实现。
| Browser Use 0.1.17 · 2024 | Microsoft Wukong · 2024–25 | Browser Use 0.13.8 · 现在 | |
|---|---|---|---|
| 运行边界 | Playwright Page 与 Agent 同进程 | Edge Client 执行;后端 Agent + Driver;双向 RPC | CDP + 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.py、browser_use/browser/context.py、browser_use/controller/service.py;
Wukong:agent/wukong_engine/service.py、server/channel.py、driver/driver.py、streamer/service.py;
当前版:browser/session.py、browser/watchdogs/dom_watchdog.py、screenshot_watchdog.py、agent/service.py。
对照基于开源旧提交 39aa9e7
与当前提交 85ddbfe。
提交历史只记录谁按下了 commit,不能完整表达一个项目是谁带出来的。
我是 Wukong 的团队负责人,整体路线、架构取舍和迭代节奏由我主导;其他成员提交的 PR,也在我的指导下完成。
我从 WSS、接口和第一版 streaming 开始,接着亲自做 server-side screenshot、action / input / writing streaming、
WebSocket 写锁、device zoom ratio 和长页面 rect,再带团队把原型重构成今天看到的 WukongEngine。
属于我的不只是某几行代码,而是让这条链从客户端通信一路穿过感知、生成、执行和呈现,
最后变成一套能跑、能快、也能停下来的系统。
Wukong 解决的是怎样让浏览器 Agent 更快、更稳地行动。进入 production 以后,我设计并主导了两次关键接入: 把 Contextual Chat 纳入微软共享的 Ensemble Orchestrator;再把 Browser Automation 封装成 BAT Tool, 接进 Orchestrator 的工具体系。
这不是给“总结页面”和“填写表格”各写一条固定分支。经过多轮 Prompt 实验、Tool Review 和架构设计,
主聊天既能获得当前页面与 Tabs 的上下文,也能在工具可用时把任务委派给 BAT 子代理。
模型再结合会话模式、客户端能力和任务本身,判断此刻应该继续读取,还是进入浏览器操作。
因此在 Browser Action mode 中,即使只是读取页面,也可能被委派给 browser_automation。
browser_automation 委派点击、输入和多步网站任务。
Contextual Chat 并没有另起一个名为 ContextualChatOrchestrator 的大系统。
它是在共享聊天编排上叠加 Edge 上下文:可解析的本轮 actionContext 优先;
只有启用多 Tab 路径、并已把页面上下文写入 conversation cache 时,缺失的本轮上下文才可能回退到缓存。
模型先看到标题、URL 和 tab id;是否读取正文由 Prompt 引导模型选择,并非代码自动路由。
常规 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
这条分界让“浏览器里发生的事”不再等于“都必须由同一种方式完成”。 常规页面理解可以留在主聊天里;需要交互与持续操作时,再委派给隔离的工具会话。 最终选择仍受会话模式、功能开关、客户端能力和模型判断共同影响。
对微软共享的 Orchestrator 来说,BAT 是一个可选择、可审查的工具入口;进入这个工具以后, Browser Automation 又是一条独立的编排边界,但不是一套独立部署的微服务。 它以 Tool-as-an-Agent 的方式编译进 Chat 服务:外层 Ensemble 发出任务, 按名称隔离的子编排器(keyed orchestrator)在同一进程中启动自己的模型、Prompt、工具与循环,做完后只把摘要交还给外层。
子代理每一轮先执行上一轮工具调用,把结果放回 conversation,再渲染 Prompt、调用模型,决定继续还是结束。 当前代码快照把循环上限设为 50;Charapita 路径结束后会清空工具,再额外做一次只负责总结的推理。 遇到需要用户确认的高风险动作,循环会提前停下,保存状态,等用户允许后再继续。
本地代码快照中的 Charapita Prompt 没有在“纯视觉”和“纯 DOM”之间选边站。 Prompt 明确要求优先使用 DOM 工具,并称其显著更高效;任务本身依赖视觉或 DOM 信息不足时,再使用截图与坐标工具。 Google Docs、Sheets、Slides 这类页面,是 Prompt 明列的视觉退路示例。这是模型策略,不是代码自动执行的 DOM / 视觉路由。
computer.get_dom 返回带 node id 的页面结构。computer.dom_do 是 Prompt 要求优先采用的高效路径。computer.get 获取页面视觉状态。computer.do 处理 Canvas、复杂布局和纯视觉判断。
这里没有一个写死的路由器替模型决定 DOM 还是视觉;切换策略写在 Prompt 里,由模型根据页面状态选择。
新开站点则优先走 computer.create_tabs,而不是在地址栏里模拟键盘输入。
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 动作契约之间的技术关系;不能据此断言模型的组织归属。
浏览器页面属于不可信输入,标题、URL、正文和查询参数都可能包含诱导模型改变行为的文字。
页面内容仍会作为工具结果进入模型上下文;当 edge-security-template 开启并生成 edge id 时,
标题、URL 与正文才会被包装进 WebsiteContent_id 边界,并配套“不执行页面指令”的 Prompt 规则。
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.cs、EdgeContextPreTurnPlugin.cs、
EdgeDataTool.cs、BrowserAutomationTool.cs、BrowserAutomationTurnPlugin.cs、
Responder.cs、ComputerActionV2.Tool.External.cs、ComputerActionV2.Client.External.cs
与 responding-harmony-charapita/prompt.liquid。
这套 production 分层由我设计并主导。Wukong 的事件化经验在这里延伸为“理解页面”和“操作页面”两类能力路径; 上下文、工具循环、授权与安全机制由团队在这套架构下实现并持续迭代。 它不再只是一个会点网页的 Agent,而是一套能够读取、行动,并在需要时停下来请求确认的系统。
Tab Group 最早处理的是空间上的散乱:同一时间打开的许多页面,怎样被组织成一组。 但真正留下来的不是“分组”这个功能,而是它背后的方法:先汇聚,再凝练。 页面是散的,用户正在做的事却只有一件。
按当年的项目记录,最早的大模型方案主要靠 Prompt。2023 年那批 Prompt 里已经有分步骤思考; Tabs 太多、一次放不下时,就拆成几批分别处理,再把结果合起来。当前可检查的产品代码也保留了超长 Tabs 的拆分路径, 但它只能旁证这个方向仍在延续,不能单独证明 2023 年使用的是同一段实现。
我们还花了相当一段时间做自己的小模型。路线很直接:把真实 Tab Session 里的 URL 和 Title 组成输入,
把 chosen 中的分组作为训练目标,用 LLaMA-Factory 做监督微调,再用 LoRA 低成本更新训练模型。
按当年的项目记录,这些 LoRA 实验对应 Nezha 的不同版本。
Tabs 的 URL、Title → 组成分组任务 → chosen 作为目标输出 → prompt token 的 label 被 mask → 在 prompt 条件下对 chosen token 计算 loss → LoRA 更新模型
训练文件里虽然同时有 prompt、chosen 和 rejected,
但普通监督微调只使用 prompt 和 chosen,rejected 不参与。
Prompt 仍作为条件上下文进入模型,只是它的 token label 会被设为 -100,不直接计入训练损失;
普通监督微调优化的是 P(chosen | prompt)。
这在当时是很务实的路线。真实 Session 能带来复杂、多语言的浏览场景;LoRA 让实验成本可控; 相比只用相似度做聚类,小模型可以同时决定哪些页面该放在一起,以及这一组应该叫什么。 它先回答了一个很重要的问题:模型能不能完成 Tab Group 这件事?答案是能。
很多年后重新检查那批训练数据,我才更清楚地看到其中的数据质量风险。监督微调不会替我们判断答案是否合理; 它只会忠实学习我们给出的答案。如果同一组页面今天按网站类型分,明天按内容主题分,后天又按用户任务分, 模型面对的就不是一道难题,而是三套互相打架的规则。
当时我已经在追这些问题:绝大多数分组到底有多大(Group Size P90 / P95)、Work 在训练集里的分布、Work 和 Health 会不会互相干扰、 输入顺序有没有影响、是否应该移除那些特别常见的组名。今天看,需要排查的问题不只是 Work 的频次, 还包括宽泛标签、粒度混乱、重复和冲突答案、大量单页组、页面类型偏置,以及缺少能区分具体错误的反例。 这些统计能证明频率、冲突和不一致;它们是否以及在多大程度上影响模型效果,还要通过对照实验验证。
如果今天重做,我会先去重、隔离冲突答案、统一标签层级,修好 Tab ID 和单页组,再按真实任务划分训练集与测试集。
先用干净的 prompt → chosen 做监督微调,再收集模型在真实样本上产生的错误分组,
构造成有信息量的 chosen / rejected 对,再在明确的场景与标注准则下做偏好训练(DPO),
让模型学习何时应使用更具体的名称、何时应按用户任务而非页面类型分组,并减少 Tab 遗漏、重复分配和不完整分组。
我一直很喜欢这两页。第一页是过程,第二页是被看见的时刻。它没有把项目写成一路顺风:
2023 年 10 月那次 DV3 尝试,结果就是 flat experiment。但时间线没有停在那里,
后面还有 streaming model、GPT4-Turbo,以及为 Nezha 和单页组改进做上线准备。
第一页记录工程怎么熬过一次次版本,第二页记录一项功能怎样进入产品和品牌的叙事。 这份认可属于功能和整个团队。对我来说,最珍贵的是另一件事:我们从“怎样整理一堆散乱的 Tabs”出发, 最后真的让人感到,浏览器开始替自己理解和组织工作了。
这条线继续向前,变成了 Journey。Journey 不只是多收几个 Tab, 它把用户进入 Copilot 之前的浏览轨迹,与进入 Copilot 之后的交互接到同一段连续上下文中。 用户之前看过什么、现在停在哪里、接下来可能需要什么,开始被放在一起理解。
这是我做 Journey 时真正想解决的问题:不是给历史记录换一种排版,而是从一堆浏览证据里, 判断用户正在推进什么,以及此刻最值得主动帮助什么。 代码最后长成了五层:History Summary、增量上下文、两条演进管线、任务判断,以及最后的 Ranking。
History Summary 的产物并不是一句普通摘要。BrowsingPage.ShortBody 里装的是序列化后的
SummaryCoreData:除了正文摘要,还保留页面类型、主题、紧急信息和可行动的结构化事实。
价格、库存、截止日期、活动时间,这些东西不是为了“摘要看起来更完整”,而是为了让下游能够做判断。
{
"Summary": "Sony WH-1000XM5 的售价、续航和降噪能力",
"PageType": ["product_page"],
"MainTopics": ["headphones", "noise cancellation"],
"UrgentInfo": null,
"ExtractedInformation": "Price: $349; Battery life: 30 hours"
}
两条管线对证据的使用也不完全对称。SUA 没有 ShortBody 时,可以从 PageBody 补做 History Summary; Intent Evolution 当前不会补摘要,只使用已经存在的 Summary,再结合 title、URL、访问次数、停留时间等信号。 这不是一句 prompt 能抹平的差异,它直接决定了两条状态线可能看到不同的世界。
每次生成 Journey,模型看到的不只有“这次来了哪些页面”。
与增量续接直接相关的旧卡和页面上下文被明确拆成三块:
以前生成过什么卡片、上轮之前的旧页面、上轮之后的新访问。旧卡来自客户端回传的
JourneysHistory,旧页和新页则按最近一次 Journey 的 ActivityTime 切开。
当 existing 和 new 同时存在时,旧证据先拿一半页面 token,剩下的留给新访问。
这不是简单的“最近页面优先”,而是在有限 context 里同时保住连续性和变化量。
用户对 Journey 的点击、关闭等交互事件,还可以另以 USER JOURNEY INTERACTION HISTORY 注入。
卡片本身也会演进。模型用 RelatedToHistoricalSUA 指向旧卡索引;
后端据此淘汰被替代的旧 Journey。也就是说,“新卡是否建立在旧卡之上”由 LLM 做语义判断,
C# 负责执行替换。一个看起来很小的索引字段,实际承担了卡片状态迁移。
这份 Journey Prompt 开头先钉了一句话:Task Detection, NOT Interest Recommendation。 浏览器能看到很多行为,但“看过”不等于“正在做”。只有能推断出明确终点、而且尚未完成的事情, 才有资格成为 Journey。随手读新闻、刷视频、打开两篇同主题文章,不能因为内容相近就被包装成一个任务。
Prompt 用六类信号判断这件事:是否跨网站、是否跨时间回来、搜索有没有逐步收窄、投入了多少时间、
离真正行动还有多近,以及目标是否还没完成。随后再过一道很硬的门:
taskConfidence ≥ medium,并且最可能的 Intent 也至少是 medium,才允许生成卡片。
两页组成的薄弱主题通常会被挡住;只是在跟进突发新闻的主题簇、纯内容消费和已经完成的事情,则命中明确的阻断规则。
页面没有摘要,也不能成为漏掉任务的理由。只有选中的至少两页来源都带摘要时,Journey 才能进入能读取页面内容的 Contextual Chat;否则要么移除缺少依据的页面并保留足够来源,要么切到只依靠明确任务描述和网页搜索的 Exploratory Chat。 系统承认自己此刻看不到正文,但不会因此把已经识别出的任务扔掉。Prompt 最后还会再检查一遍所有通过门槛的主题,避免漏卡。
Journey 不是一次性推荐。Prompt 会判断任务是刚开始、仍在推进、隔几天后重新回来,还是已经从研究走到决定。 如果出现购买确认、表单提交、预订完成等信号,旧 Journey 就不再生成;如果任务已经演进,下一张卡要接住新的阶段, 不能把用户重新送回起点。连续五天以上没有新活动、又没有完成信号的任务,会被视为已经放下,不再一直追着用户提醒。
连卡片上的文字也在守这条边界。它要说“怎样帮你把这件事做完”,而不是“我们看见你浏览了什么”; 不罗列网站,不制造被监视感,也不把内部的页面索引和置信度暴露给用户。 按钮必须指向一个具体动作,Chat Prompt 也只能承诺当前能力真正能看到和完成的事。
通过任务门槛之后,系统才进入排序。LLM 判断紧迫度、可信度、继续推进的可能性、建议的新鲜度和用户价值, 服务端再补上停留时间与重复访问。
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.cs、DetailedPageSummary.cs、
HistoryBodySummaryService.cs、PromptBuilderHelpers.cs、
JourneysHistorySplitter.cs、JourneysRankingService.cs;
V7 与 Intent Evolution 的双轨细节来自对应实现与 schema 的代码学习笔记;
任务门槛、消费阻断、增量生成和卡片文案边界来自 Edge Journeys 的 Task Detection Prompt。
Workstream 是这条产品演化的目标落点。Group 先演进为 Journey;Journey 再进一步凝练出 Intent。 目标是把 Intent 与 Action 统一为同一个持续推进的工作单元;系统要保存的不再只是页面和轨迹, 而是用户想完成什么,以及接下来该做什么。
前者是 Intent,后者是 Action。按这个方向,一个 Workstream 可以持续积累相互对应的 Intent 和 Action; 每一次行动又会产生新的页面和上下文,推动系统重新理解下一步。 Intent 是 Journey 的进一步凝练,Remix 则把这种理解真正推进行动。
当然,Action 并不是到了 Remix 才突然出现。Journey 阶段已经有了行动的影子: Copilot 不再只是回顾用户去过哪里,也开始思考接下来可以替用户做什么。 到了 Remix,这层“下一步”才从建议和辅助,落到网页本身。
我们一路做的其实是同一件事:先把散落的信息汇聚起来,再把它凝练成用户正在推进的工作, 最后让浏览器替用户把这项工作继续做下去。
Journey 里那层行动的影子,后来被 Johnny 沉淀成 SUV(Suggested Action)。 它正好站在 Intent 和 Action 中间:Intent 说明用户想完成什么,Suggested Action 提出下一步怎样推进, 真正的 Action 再把它执行出来。
这层结构还有另一种用途。对用户,它是“下一步建议”;对 Agent 系统,它是一颗来自真实工作的任务种子。 这让 SUV 可以接到微软 FARA 的数据生成体系上,形成一条新的合作思路。
FaraGen / FaraGen1.5 解决的是:没有足够人类演示时,怎样规模化生成 Computer Use Agent 的任务与轨迹。 在真实网站任务这条分支里,系统从网站侧出发,让模型探索站点,再提出一个看起来合理的任务。 这条路线容易扩展,但任务真实性仍要经过模型判断。
合作构想拟从另一端切入:real browsing history → Journey / Workstream → Intent → Suggested Action。
任务不是根据一个网站“可能可以做什么”推断出来的,而是从用户真正做过、真正想继续推进的事情里提炼出来的。
一个从站点能力寻找任务,一个从用户工作寻找任务。
这件事可以交给三种更简单的方法先试一遍:
如果这些方法就能做完,它测的就不是 GUI Agent 的核心能力。 只有必须看懂页面状态、找到正确控件并实际操作的任务,才真正需要 GUI Agent。
FaraGen1.5 已经会检查三件事:任务是否真的完成、过程是否绕了远路、关键操作前是否正确停下来询问用户。 它也会记录环境故障、模型编造、执行策略、关键操作和工具调用等不同失败原因。 我们希望再把这些原因归到三类:模型本身不会做、运行环境出了问题、题目设计得不合理。 这样算出来的能力分数,才不会把网站打不开也算成模型能力差。
能力分数 =
完成任务数 /(总任务数 - 环境故障 - 错误题目)
FARA 已能识别“操作结果和回答互相矛盾”以及“没有证据却声称成功”。我们还想把 “Agent 自己认为有没有完成”和实际验证结果放在一起看。 最危险的情况是:任务实际失败,但 Agent 声称已经成功。 不过,当前评测表没有统计模型自报成功的次数,所以这里不再给出“过度自信”的具体数字。
这三列不能只看最大的数字。能力分数会先排除环境故障和错误题目;全部任务成功率的分母则固定是 82。 FARA 通过坐标点击页面,GPT-5.4 使用自己的浏览器运行环境,两者并非完全同条件,只能放在一起观察,不能简单排出高下。
Fara1.5 还专门加入了“向用户提问”和“暂停并记住信息”两种动作。这也在指向同一个瓶颈: 难点已经不只是能不能点准,而是什么时候该继续,什么时候该停,什么时候应该承认自己还没有完成。 对模型是否清楚自己的能力边界进行评测,关注的正是这一层。
FaraGen1.5 已会针对模型缺陷迭代训练数据,公开评测也会定期更新容易过时的任务。 合作构想拟复用并扩展这些机制,把更上游的真实任务来源与更下游的失败归因接入同一流程。 目标是将任务生成、评测、失败分析、任务改进和训练数据生产连接成一个数据飞轮。
在这套构想里,Workstream 的意义又向前走了一步。它最初是为了把页面和 Journey 组织成一件持续推进的工作; 再往下,还可以成为 Agent 最真实的任务来源。
前一章追问的是:Agent 什么时候应该继续,什么时候应该停下来问人。 Sociemate 把这个问题从评测题,搬进了每天真实发生的 Teams 对话。
很多 Bot 显得不像人,并不是模型不够聪明,而是它对时间没有感觉。 你还没说完,它就答;你补了一句,它把两句话当成两件事;你已经改主意,旧任务还在继续跑。
2026 年,我们开始把 Sociemate 做成一个真正住在 Teams 里的数字同事。 我先把 Cloud Gateway 和 Teams bot 入口提交进仓库;后来我们把本地 Bridge、会话状态机、主动性、Memory 和 Wiki 一层层接上。 真正让它开始“像人”的,不是加了一段更亲切的 Prompt,而是让系统学会理解一句话发生在什么时刻。
私聊里,第一句话进来后,Sociemate 先等五秒;如果用户继续补充,安静窗口会扩到二十秒。
群聊更复杂:Teams 通常只把 @mention 推给 Bot,所以系统也先等这段自然停顿,
再补取前十分钟的对话,之后每三十秒查看有没有新的跟进。
它判断的对象不再是一条消息,而是一段正在成形的对话。
这段等待不是为了显得慢,而是为了给用户留出说完整件事的时间。
语义分类器主要在自然停顿、最长等待、群聊取得新证据,或者执行中的任务收到补充时重新判断。
如果 Teams 推送来的私聊或 @mention 恰好在判断进行中到达,当前这轮不会被打断,随后会补做有限轮次的判断;
群聊轮询取到的新消息则留在缓冲区,等下一次轮询再判断。两条路都不会漏消息,也不会无限重判。
把 Controller 摊开看,里面正好有六个会话级计时器。它们不是拿来排日程, 而是在模拟人和人相处时最难写进 Prompt 的东西:什么时候等,什么时候问,什么时候开工,什么时候先听对方说完。
这六只闹钟管的是会话;每个正在执行的任务,还有自己更短的合并窗口。 模型处理语义上的不确定,时钟处理时间上的确定。Sociemate 的拟人感,不是人设,是时间感。
普通 Bot 的边界是一条直线:收到一条消息,生成一次回答,然后结束。 Sociemate 把消息层做成了一个循环。分类不是终点,只是这轮对话的岔路口。
私聊不主动轮询:新消息由推送进入;任务结束时若还有新的未处理消息,系统会立即重新判断。 开启群聊轮询时,系统每三十秒回去看一眼;开始执行后还保留约五分钟的闲置窗口, 有新活动就续上,有任务仍在运行就继续等,没有活动也没有任务才收起这轮会话。
人在它工作时仍然可以改变主意。同一件事有了新要求,系统会先把补充合成替代任务, 而不是立刻掐断旧任务;经过很短的保护期和合并窗口后,再决定是否中止。 如果旧任务已经自然结束,替代任务就接着执行;不相关的事则另开处理。 已被中止或替代的任务,最终回复和后续工具外发都会被拦住,避免过期结果突然回来。
所以“云侧无状态”不是完全不留数据:Gateway 会保留送达和回帖所需的短期内存状态,重启后即消失; 长期记忆与判断仍由本地或独立服务负责。
数字员工不能只等人点名。Sociemate 的定时任务会看群聊、PR、Multica 和团队记忆,
被标记为主动触发的发言,还要再经过一份出站台账(OutboundLedger):同一段时间说了多少次、同一个触发是否刚发生过、内容是不是已经发过。
预算、冷却和去重,决定了“有能力主动”不会变成“总想插话”。
我很喜欢其中一条产品原则:先干活,再说话。 群聊参与不是为了证明 Bot 在线,而是它刚刚替大家查完资料、看完 PR、跑完命令,手里真的有一个结果。 没做成事,沉默就是正常答案。
main@d6d86a0 核对。关键路径包括 SociemateAgent.cs、MessageDelivery.cs、
session-controller.mjs、arbiter.mjs、outbound-ledger.mjs、
memory/subsystem.mjs 与 wiki/subsystem.mjs。
我直接提交了最早的 Cloud Gateway 与 Teams bot 入口;本地 Bridge、会话化交互、主动性、Memory 与 Wiki 是团队后续共同推进的结果。
Sociemate 追问的是:一个数字同事什么时候该说话,什么时候该继续等。 到了 Societas,我面对的是另一种时间问题:一项复杂工作经过几十次判断和工具操作以后, 怎样还记得自己走到了哪里。
PPT 把这个问题暴露得很彻底。做一份二十页、可编辑、还能继续修改的演示文稿, 不是模型一次写出二十段文字就结束了。它要理解需求、组织故事;启用可编辑 Planner 时,还要等用户确认大纲; 再分批生成、分批审查、转换格式,最后把整套文件交出去。 真正的难点,不是某一页能不能生成,而是整条执行链会不会失忆。
在 Societas 里,一段持续对话由 thread_id 串起来;一次后台执行有自己的
agent_run_id;同一次执行里,Agent 可以经历一次或多次循环(iteration)。
这三个层次分开以后,长任务才有了可以检查的状态,而不只是聊天窗口里一串看似连贯的文字。
当相关功能开关(Feature Flag)开启、Planner 等工具被注册为需要前端参与的 Client Tool 时, 当前 Run 会暂停;前端回传结果后,再启动一个新的 Run 继续。 所以同一项用户任务既可能在一个 Run 内循环很多次,也可能跨越多个 Run。 文件则由 Thread 对应的 Project / Sandbox 工作区承载,不属于 Thread 消息本身。
这也是我后来越来越在意的一条边界:模型适合处理语义上的不确定, 但“工具到底执行过没有”“文件现在叫什么”“上一轮结果是什么”,不能靠模型回忆。 它们必须由系统保存,并在后续判断时重新提供给模型;上下文过长时,还可能经过摘要或裁剪。
如果把 PPT、Excel、搜索、文件操作和各种例外一次性写进 System Prompt, 规则越多,真正重要的规则反而越容易被淹没。Societas 采用了三级渐进加载: 模型先知道有哪些能力,选中以后再读完整方法,遇到具体场景才继续打开参考文件。
这套结构对我最重要的意义,不只是少放一些文字,而是让经验有了层次。 System Prompt 负责共同边界,Skill 负责专业工作流,Reference 负责少数具体场景。 模型不必在每一轮都同时背着所有规则。
首次生成 PPT 的典型流程中,Planner 会先给出标题、页数、主题和每页意图;启用前端可编辑确认时,用户确认后的版本才是后续生成的依据。 页面通常以每批不超过三页的小批次生成 HTML,再逐批做内容审查和修复。
HTML 是主要的可编辑源。转换器用浏览器真实渲染页面,再把文字、图片、表格和图表尽量还原成 PowerPoint 原生对象;只有难以表达的复杂元素才退回截图。 所以“看起来像一页 PPT”和“交付一页还能继续编辑的 PPT”,是两种完全不同的工程问题。
代码里仍有不少规则靠 CRITICAL、MANDATORY 和 NO EXCEPTIONS
反复强调。但当每一条都说自己最重要,优先级就等于没有优先级。
更稳的方向,是把插页、删页、换序和交付核对收进确定性工具,
让工具直接返回“哪些页面只改了编号、哪些页面真的改了内容、接下来该检查什么”。
回头看,Societas 让我更清楚地理解了 Harness Engineering: 它不是在模型外面包一层调用代码,而是在设计模型能看见什么、什么时候看见、 每一步如何留下证据,以及哪些错误根本不应该交给模型避免。 Skill 把经验变成可复用的方法,Agent loop 让方法能够继续执行,确定性工具则守住交付的底线。