进任何产品先找视频入口,片头两秒就判去留;容不得廉价感、填充镜头与营销腔。
一个足够像人的
虚拟用户
engine 的全部立身之本,是一支会决策的箭头。不像人,则上层(场景泛化、产品评测)一切失效。
没有显式的 goal 字段。目标隐式涌现于自由文本的内心独白与历史之中——显式化会让仿真
变机械、变 task-driven、变得不像人。认知链就是 history 本身,默认不压缩:
summary 与 RAG 会破坏因果结构,得不偿失。
数据不可变。每一步产生一个全新的 SessionFrame,history 只增不改——利于回放、
分叉与审计。UserSimulatorAgent 是其上的异步 OO 外壳;fork 只是「截断 + 换 id」
的纯数据操作。一个实例 = 一条逻辑时间线;并发只发生在不同实例之间。
规划 / 执行
两层拆分
规划层(纯认知)只决定用户想做什么,唯一对世界的动作是 operate;执行层接到语义意图后自己落地。两层都在 engine,simulator 只提供活体沙箱。依赖单向:backend → engine → simulator。
operate 语义意图,执行层按端形态选实现(视觉端接地子 agent / shell 直通),落地真实端后观察回灌闭合因果链A 执行层是「手」,不是第二个大脑
认知、偏好、决定下一步 100% 留规划层;执行层只把一个客观意图落地。边界压在价值判断上——operate 明令执行层只做客观达成条件,「觉得好不好 / 值不值」必须回规划层自己定。单 agent 认知未被拆分,只是具身被委托。
B uses 单一归属执行层
外部注入的高层能力(write_file / 业务动作)只由执行层感知,规划层永不感知——一个能力恰好一层,杜绝「两层都能调」的双路径(模型摇摆、cost 重影、职责不清)。规划层只发 operate(意图)。
ReAct:观察 → 思考 → 行动
每一步都是一次 decide():把 (persona + history + observation + runtime) 经 user-model 推理成 (thought + actions),连同该步 cost,追加一条 StepRecord。
run() 自治循环:open 开场 → step/execute 闭环 → 三个控制出口收束# 开场一等环节:跑 runtime.init 得首步观察,幂等(只开场一次) observation = await self.open() while True: await self.step(observation) # 推理一步,追加不可变 StepRecord record = self._frame.current verdict = _control_verdict(record.actions) if verdict == "done": return await self.end("succeeded") if verdict == "give_up": return await self.end("abandoned") if len(self._frame.history) >= max_turns: return await self.end("timeout") observation = await self.execute(record.actions) # operate→执行层 → 下一步观察
推理走 tool-calling:把规划层词表——控制动作(speak / done / give_up)
加上挂端时的一等 operate——以工具定义 bind 给模型,模型每步产出一句内心独白(content = thought)+ 一个 tool_call(= action)。
关键在于 decide 只取 tool_calls,不在模型循环里执行——执行交给
agent.execute 与上层调度器,保 decide 纯数据、frame 可 fork、多 agent super-step 可控。规划层不暴露任何 use(外部能力归执行层)。
跨模型鲁棒性:
四道防线
不同 provider / proxy 各有怪癖——reasoning 藏思考、把动作写进文本、并行调用走幻觉命名空间。decide() 用有界的修复策略兜住,绝不让 agent 陷入「以为做过」的幻觉链。
瞬时错误退避重试 _ainvoke_with_retry
rate-limit / 429 / timeout / temporarily 类瞬时错误:最多三次、0.5×(i+1) 退避重试;非瞬时错误立即抛出,不空转。
整步失败静默,而非杜撰 except → thought="", actions=[]
LLM 调用 / 工具解析彻底失败时,退化为「沉默一步」:不炸整条会话,也不编造一个动作。沉默是合法的稀疏默认,宁可什么都没做,也不污染因果链。
broken-ReAct 修复重试 _intent_in_text
零工具调用、但独白里写了「工具名(…)」——模型在模仿轨迹格式而非真行动(实测 gpt-4o 系高发)。带上原输出 + 修复提示做一次有界重试(仅一次,不循环)。
空独白修复 _elicit_monologue
reasoning 模型把思考藏了、content 为空但有 actions:就这步动作向它要一句内心独白(= 模型自己的话,非代码杜撰),保住对外的「人味」。补不到才告警。
# OpenAI 系两个已知怪癖(gpt-4o 群体 e2e 高发):不解开则 actions 落空 for tc in tool_calls: name, args = tc["name"], tc.get("args") or {} # ① 并行调用走幻觉命名空间 multi_tool_use.parallel,真调用藏在 args.tool_uses if name.split(".")[-1] == "parallel" and isinstance(args.get("tool_uses"), list): for tu in args["tool_uses"]: inner = str(tu.get("recipient_name", "")).split(".")[-1] if inner: actions.append(Action(type=inner, args=tu.get("parameters") or {})) continue # ② 工具名带 functions. 前缀 actions.append(Action(type=name.removeprefix("functions."), args=args))
喂给模型的,
是一个人的处境
system 只讲「你是谁 / 处境 / 怎么结束 / 怎么行动」,能力细节交给 tool 定义。三处工程把 token、视觉与人格漂移一并管住。
折叠 早期观察省 token
历史里距今超过两步的,只留「想 / 做」;最近两步才带完整观察。过往截图只留文字占位,当前观察的真图另作 image 块。
配图 上一步状态图
build_messages 把上一步与当前观察的 image Part 作为 image_url 送进 vision 模型——让它能「看 UI」做决策。本地截图仅在发送时即时读为 data-url,不进任何持久化。
重锚 长会话身份重申
注意力随轮次衰减,system 里的人设会被长 history 稀释(persona-drift 实证 8–12 轮显著)。_anchor 在 ≥4 轮时于生成点附近重申「你是谁 + 此刻状态」。
"你在扮演一个真人(不是 AI 助手),以第一人称做出这个人真实会做的反应。" # ……persona(profile / skills / memory)/ 处境 / 眼前 ……(无外挂投入度档位) "原则:保持这个人的个性,别变成『积极的平均人』——平庸就否、该追问追问、" "该核实核实、该弃就弃;多数真人多数时刻只看不说,沉默是常态不是例外," "按这个人的真实频率开口——人设话少,就让大多数步真的沉默。" # 每步先有个内心活动:像真人脑子里一闪而过的半截念头——往往很短、跳跃、口语, # 有时甚至什么都没想(那就留空)。别写成条理分明的分析或要点罗列。
实证修正(2026-06,e2e_usa_fidelity 对照实验定):把「稀疏为默认」写进 system 为显式常态后,
活跃度轴从「全员 88–100% 零方差」还原为「潜水党 14% / 话痨 100%」,且任务 / 问卷语境不受抑制,token 反降约 30%。
更进一步,外挂的投入度档位 AgentMode 已被整体删除——认真 / 摸鱼不再由参数钉档,纯由 persona 文本自洽涌现,
TriggerRequest 只剩 persona + scenario。少一个会撒谎的旋钮,异质性回到它本该来的地方:人设本身。
执行器
按端形态选实现
执行层不是固定的 LLM 子循环。它按端有没有「接地鸿沟」(意图离精确动作隔着像素 / 元素的不确定性),在会推理的接地子 agent 与无 LLM 直通之间选——对外仍是同一个 operate 动词。
视觉端
ExecutionAgent · 接地子 agent有接地鸿沟:意图(「把鼠标加进购物车」)要落成 tap / click,得看屏、认元素。执行层是会推理的子 agent,多步落地——大圆镜 8 人走的就是这条。
- 规划层发 operate(意图)
- EndDriver observe(截图 + 元素清单)
- do_tool_step 强制单工具调用
- 执行 → 重新观察 → 续跑
mobile / desktop 用认图模型 stepfun;运动步压缩——8 次 tap = 规划层 1 决策。
shell
DirectShellExecutor · 直通无接地鸿沟:命令即精确动作,「一条命令」本身就是一个认知决策(工程师就以命令思考)。执行器不挂 LLM,把 operate.command 当字面命令跑一次。
- 规划层(gpt-5.5)亲笔写命令
- 跑一次(带超时)
- 完整 stdout/stderr + exit code 原样回灌
- 写整段文件用
write_file(绕开 heredoc)
分层逻辑前移到规划层、执行器退化成直通;文本端 operate 的执行 cost = 0。
operate 只能说要达成什么,绝不说怎么操作。
命令一旦写成「点击 ADD TO CART」「在输入框输入并发送」,执行模型就镜像成裸 Click/Fill 原语(动词对上了),绕开封装好的健壮 use;翻车 → 规划层更不信任 → 命令更像素级(曾退化到给坐标)→ 更勾原语。恶性循环。
修后改成意图级命令(「问 Kimi:<原话>」「加购 2 件」),live 实测:procurement 按键措辞归零、裸原语 22→8;Kimi 从「在输入框输入并发送」变「问 Kimi:…」、裸原语 55→0。
c0ac460 为视觉端接地子 agent 补的三件事
① 按端分系统提示
mobile / browser / shell 各自一段 EndDriver.system + hint:手机讲「Tap 用编号、误入弹窗先关闭」,浏览器讲「Click/Fill 只用 ref、读正文优先 ReadPage」,终端讲「看 exit code、别套 GUI 规则」。
② 阅读进度感知
mobile observe 把已向下翻几屏、本屏新增可读文本几条、是否出现结尾 / 评论 / 推荐信号、下滑后文字是否重复显式喂回模型——治「长文还没读就声称读完」与「到底了还在傻翻」。
③ 失败摘要人味化
回给规划层 persona 的不是执行层术语:_humanized_failure 把「达到最大步数」翻成「我往下读了 5 屏,还没看到明确结尾」——执行明细藏起来,规划层只见一句人话结论。
progress = (
f"阅读进度(本次执行):已向下翻 {self._down_swipes} 屏;"
f"本屏新增可读文本 {len(new_terms)} 条;"
f"{'出现结尾/评论/推荐信号' if end_signal else '未见明确结尾信号'}"
f"{';下滑后文字重复,可能到底' if repeated_after_down else ''}。"
)
# _READ_END_MARKERS = ("评论区", "相关推荐", "阅读原文", "没有更多", "全文完", …)
轨迹 执行层做了什么,看得见
执行层每次 operate 的逐子步明细(真命令 / 端原语 / use 调用 + 结果 + 截图)经 rt.op_trace 回流,折进 StepRecord.operate_log。这是审计 / 前端显示通道——admin run 详情据此渲染折叠步进时间线、shell 真命令;但不回喂规划层(规划层只见意图摘要,单一归属)。
cost 一帧含两个模型
执行层 token 经 rt.cost_sink 回流,agent.execute 每步并进 StepRecord.cost——一帧同时含规划(gpt-5.5)+ 执行(stepfun / gpt-5.4-mini)两个 model key,按模型名拆分。shell 直通端执行 cost 为 0。
别演成
「积极的平均人」
LLM 默认会把用户演成积极的平均人:高估行动频率、回避负面、抹平个性。
- 稀疏稀疏为默认:多数步
actions=[](真人正向率很低)。别每步都积极行动。 - 负面负面可表达:
give_up、抱怨、对抗性speak必须演得出。 - 异质异质性保真:鲜明 persona + 保真 history,保住长尾个性,不收敛成「平均人」。
- 校准有真实行为数据(L2 场景)时,把该用户真实 base rate 喂入校准。
教训(2026-06 实证):测什么,保什么——没测的轴会塌缩成「积极的平均人」。 三场景全绿的同时,潜水党发言率却到了 75%、话少者 100%。评群体涌现不能替代评个体保真, e2e_world_*(群体)与 e2e_usa_fidelity(个体保真,评 §1 箭头本身)两层都要跑。
大圆镜 · 8 个真人,
8 台手机
8 个差异化 persona × mobile 群体实验:同一个「开放刷机」场景,让八种不同的人各自用真实习惯去刷一个科普 App。异质性是这里唯一的主角——投入度不再有外挂档位(AgentMode 已删),认真 / 摸鱼全由 persona 文本自己涌现。
看什么都先防坑,对广告、套路、夸大数字格外警惕;慢、稳、要核实。
愿意把一篇长文从头读到尾,遇到讲不通的地方会停下来深究、较真到底。
刷得飞快、自动驾驶;大多数内容三秒内划走,极少停留,几乎不开口。
把每个角落都点一遍、追问到底;越投入越挑剔,是最深度也最难取悦的那个。
被画面驱动,封面不够好看就不点;图强则停,纯文字界面对她是「能忍但没惊喜」。
碎片时间刷手机,等活儿的间隙看两眼;被标题勾住才点,看一半就走。
带着审稿的眼睛刷内容,专挑事实错误、逻辑漏洞与含糊措辞,找到茬就要说。
- 群体规模
- 8差异化 persona
- 会话上限
- 18轮 / closure.max_turns
- 真实端
- mobileheadless 模拟器 + scrcpy 保活
- 并发
- ×8emulator-5554 … 5568
脚手架 launch / bootstrap_8users
引用式 API:先把 persona / scenario 建成资产拿 id,再逐台发 /runs/usa(runtime={kind, app, serial})。每跑一次会 PATCH 同步最新 persona 文本;对 adb 未就绪的 serial 主动跳过,避免在 step0 硬失败。
保活 headless + scrcpy framebuffer
headless 模拟器跑批量,scrcpy 保活 framebuffer——没有前台镜像时屏幕内容会停更。8 台模拟器在线、装好大圆镜(com.grandmirror.gamma),RUN_MAX_CONCURRENT≥8 才有真 8 并发。
一个驱动器,
统一所有群体执行
WorldRunner 是 backend 编排层全部群体执行的唯一驱动器。原先四条手搓的
execute(predict / abtest / freeform / novel)已收敛成一条 strategies.execute_world,
差异全部下沉到 World 声明 + agent 装配。
这印证了群体层契约的泛化性:「独立采样」不是另一套机制,而是 IsolatedEnvSpec
(零互投,agent 互不可见)+ lockstep 的退化形——大圆镜 8 人正是这种 isolated 介质下的独立 ensemble;
in_memory / platform 介质则是耦合互动。freeform methodology 由开放声明驱动,幂等 seed。