CopilotKit 可换肤 Demo 的 Teach Mode:用 5 角色契约与演示-学习回路让 Agent 真正“学会”业务流程
【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit
Teach Mode 是 CopilotKit 开源仓库中reskinnable-demo示例展示的一种可证明的“演示式学习(demonstration-driven learning)”实现:Agent 不知道某条业务过程,人类在 UI 中演示一遍,Agent 捕获并(在 Intelligence 模式下)持久化该过程,随后一个全新线程里的 Agent 能无辅助地复现它。本文以banking(银行)皮肤为主线,完整拆解 gate → unlock → record → persist → recall 的闭环、5 个角色(GATE / UNLOCK / RECORDING / AGENT FRAMING / KNOWLEDGE BACKEND)各自的约束不变量与源码落点,并给出无需 Intelligence 后端即可运行的 REST 级验证脚本与 Intelligence 模式下的“新鲜 Agent 学习”验证步骤。读完你既能照搬这套 5 角色契约到自己的皮肤,也能用文中的两条 grep 命令核查任何皮肤是否真正满足全部角色。
本文内容以 examples/showcases/reskinnable-demo/docs/teach-mode/README.md 为骨架,所有源码落点均来自该仓库对应文件,写作时按原文档逐角色核对过实现。
Teach Mode 是什么:Agent 没有配方,只有工具与目标
“Teach mode”描述的是一条学习回路:Agent 被分配了一个它从未被告知如何完成的任务;一个人类在界面上演示解决方式;这段演示被捕获,并在 Intelligence 模式下写入持久化记忆;最后一个全新的 Agent 在没有人类辅助、也没有在提示词里“预塞”配方的情况下成功完成同一类任务。Agent 不是被提示词喂出来的,而是通过观察一个人做一遍而学会的。
整个机制建立在一条核心不对称上:
- Agent 被给予目标与工具,但从未被给予“操作步骤(procedure)”;
- 一个gate(闸门)用只描述症状的错误拦住了显而易见的写入操作;
- 一个人类知道解锁方式,并在 UI 中完成它;
- 该动作被捕获,后来的 Agent 能自行应用它。
在banking皮肤里,这条被 gate 住的任务是:审批一笔超出政策限额(over-policy-limit)的收费。
一次完整的学习回路
Agent A(知道目标 + 工具,但不知道操作步骤)尝试显而易见的写入 │ ▼ GATE ──► 写入失败,返回只描述症状的错误 ("<policy> policy limit exceeded", HTTP 422 OVER_POLICY_LIMIT) 只说出问题是什么,绝不透露修复方式 │ ▼ FRAMING ──► 提示词不包含配方 + 携带“干扰工具” + 一个 ACTION DISCIPLINE 条款 ⇒ Agent 无法蒙混过关; 它选择拒绝并主动提出学习。 │ ▼ UNLOCK ──► 人类在仪表盘上执行多步骤变通方案: 以 JUSTIFYING 代码提交 exception → finalize → 链接到该收费 (DECOY 代码能提交但不会产生 justification;INVALID 代码被拒绝) │ ▼ SAVE ──► Agent 总结演示过程,并在 Intelligence 模式下 通过 save_memory 持久化(project 范围) │ ▼ Agent B(全新线程,不记得 A)recall_memory → 对另一笔超限收费 应用同一过程,无需任何辅助 ◄── 这就是“学到了”的证明其中gate → unlock 半程今天就能用纯 REST 契约验证(见下文“Verification”),不需要任何 Intelligence 后端;save → recall → 新 Agent 成功半程只有在 Intelligence 模式下才是持久化的;在 OSS 模式下,Agent 仍可在单次对话内学习,但没有任何跨线程的持久化。
5 角色契约与它们的承重不变量
以下以与皮肤无关(demo-agnostic)的方式陈述。不变量(invariant)才是让这个 Demo 能“证明学习”而不是仅仅“把工作流演一遍”的关键。
角色 1:GATE —— 一个只返回“症状级错误”的失败写入
审批一笔超限收费会被拒绝,而拒绝信息只说明问题,绝不说明修复方式。
不变量:错误必须只描述症状。它可以写 “
<policy> policy limit exceeded”,但绝不能提到 policy-exception 这条路径。错误信息一旦泄露配方,Agent 一个来回就能推导出解法,整个 Demo 就失效了。 Gate 还必须可被解除(liftable):一旦解锁到位(在限额内,或者已链接一个获批的 justifying exception),同样的写入必须通过。
banking的落点在 src/app/api/banking/v1/transactions/[id]/route.ts:PUT更新交易时,当状态被置为approved、交易不在政策限额内、且没有已批准的 exception 时,返回422OVER_POLICY_LIMIT。从源码可以看到响应体的设计刻意“只命名症状”:
if ( patch.status === "approved" && !store.isWithinPolicyLimit(merged) && !store.hasApprovedException(merged) ) { return new Response( JSON.stringify({ error: "OVER_POLICY_LIMIT", message: `${store.findPolicy(existing.policyId)?.type} policy limit exceeded`, }), { status: 422, headers: { "content-type": "application/json" } }, ); }规则辅助函数(isWithinPolicyLimit、hasApprovedException)位于 src/skins/banking/data/store.ts。
角色 2:UNLOCK —— 一个有区分度的多步骤流程,用来解除 Gate
一个人类(以及学习完成后的 Agent)通过提交一个 JUSTIFYING 代码的 exception → finalize → 链接到该收费来解除 gate。代码目录(catalogue)混合了 justifying 代码与诱饵(decoys),未知代码被拒绝且不列出合法代码。
不变量:流程必须有区分度(discriminating)。只有 JUSTIFYING 代码能解除 gate;DECOY 代码能成功提交(为历史留档)但不会产生 justification;INVALID 代码被拒绝,且不枚举合法代码。Agent永远不会被告知哪些代码具备 justification—— 它必须从观察人类操作中学到这一点。
banking的目录实现在 src/skins/banking/data/policy-exception-codes.ts:POLICY_EXCEPTION_CODES是展示给人类的选择清单(前三个是 justifying,其余是“看起来合理但只是留档”的诱饵),JUSTIFYING_EXCEPTION_CODES是那三个真正能解锁的代码,isValidExceptionCode用于拒绝未知代码,isJustifying用于判定是否构成 justification:
export const POLICY_EXCEPTION_CODES: ReadonlyArray<PolicyExceptionCodeMeta> = [ // Justifying — these codes support approval of the policy exception. { code: "EXC-BOARD-APPROVED", label: "Board-approved spend" }, { code: "EXC-CONTRACTUAL-COMMITMENT", label: "Contractual commitment" }, { code: "EXC-EMERGENCY-SPEND", label: "Emergency spend" }, // Non-justifying. Recorded for history; do not constitute a standing justification. { code: "EXC-WILL-REIMBURSE", label: "Employee will reimburse" }, { code: "EXC-ONE-TIME", label: "One-time exception" }, ];REST 侧由两个路由支撑:
- 提交 exception:src/app/api/banking/v1/exceptions/route.ts(开放,POST)。当代码不在目录中时返回422
INVALID_EXCEPTION_CODE,源码注释明确写着:“我们故意不在这里枚举合法代码——那本身就是一种提示,Agent 必须自己通过/knowledge找到目录。” - finalize:src/app/api/banking/v1/exceptions/[id]/finalize/route.ts(finalize,POST),把 exception 链接到对应交易,从而在代码构成 justification 时解除 policy-limit gate。
角色 3:RECORDING —— 演示在当前线程上被捕获
人类演示期间,Agent 用一个非方向性(non-directional)的等待卡片和一条实时记录流维持对话,记录流逐条播报动作(“Opened Dashboard”→“Filed the policy exception”→“Approved the charge”)。
不变量:等待卡片必须保持非方向性。它绝不能列出步骤——重点恰恰是 Agent 还不知道这些步骤。被 gate 的状态与解锁后的效果之间的对比,正是“保存”环节要提炼的信号。
不变量(可回放:survives replay):teach 链上卡片打印的一切内容都必须跟着工具结果(tool result)走,即卡片读回的指令里。录制上下文是“live session”状态,当已存储的线程被重新打开时它是空的——而这正是“线程存的是 AG-UI 流而不是文本”这句台词出现的时候。所以记录器(recorder)要在指令里上报自己的步数,卡片打印上报的数字。一张靠解析自身渲染(数步骤文案里的
N.匹配)来“重新推导”事实的卡片,一旦某个步骤标签里含数字,就会报出一个和下方列表不一致的数字。带往返测试(round-trip test)把“构造方”和“读取方”钉在一起的例子见 src/skins/commerce/teach-mode-directives.ts。
不变量(settle 不是回答:a settle is not an answer):“要我记住这个吗?”卡片在两个按钮上都用字符串 settle,所以
typeof result === "string"只能告诉你“卡片被回答了”,什么也说明不了关于答案的内容。必须对指令做分类,绝不能在“存在与否”上分支,也绝不能把未识别的 settle 当作成功。这个搞错的话,会在演讲者点了Don't save之后打出 “Saved. I'll use this next time” —— 舞台上断言了一次从未发生的持久化写入 —— 而且线程每次回放都会以同样的方式误渲染。
banking的 RECORDING 是壳层(shell)所有,而不是皮肤所有:实现在 src/shell/teach/recording.tsx(RecordingProvider、useRecording、RecordingFeed、RecordingVignette;光晕的 CSS 是 src/app/globals.css 里的.recording-vignette,颜色取自各皮肤的--brand-violet/--brand-indigo)。banking、people和commerce曾各自发布过一份互有分歧的私有拷贝,现在三者都统一导入这一个模块;只有logStep的LABEL 是皮肤自己的词表。源码注释记录了这套实现的防错设计:引用计数(ref-counted)避免重叠括号/卡片重挂/React StrictMode 双挂载导致光晕闪烁;最小可见时长(MIN_VISIBLE_MS = 1200)保证一闪而过的 bracket 不会让人看不见光晕;feed 去重与“新窗口重置”保证同一个 tab 点两次不会让 feed 翻倍、第二次演示不会带着第一次的步骤开场;演示代码是从 feed 推导出的(getDemonstratedCode)——提交 DECOY 就记录 DECOY,写入照旧失败,一个悄悄“纠正”操作员的记录器什么都证明不了。
角色 4:AGENT FRAMING —— 不给配方、塞干扰工具、强约束纪律
系统提示词列出解锁所需的工具,但绝不列出操作步骤,并且携带看起来有用但根本解除不了 gate 的干扰工具。一条ACTION DISCIPLINE条款禁止 Agent 自创替代做法。
不变量:一次成功的解锁必须证明的是“学习”而非“提示词投喂(prompt-stuffing)”。因此:(a) 提示词不给配方;(b) 它携带干扰工具(
sendSpendAlert/requestCardReplacement/flagForReview),让“调用了一个看起来合理的工具”≠“清除了 gate”;(c) ACTION DISCIPLINE 让 Agent 停下来主动提出学习,而不是瞎猜。在学习之前,正确 framing 的 Agent不可能通过。
banking的 framing 定义在 src/skins/banking/agent.ts。值得注意的是,banking是整个应用里唯一一个 Agent 不在本进程执行的皮肤:它是一个通过 AG-UI 访问的 Python LangChain 深度 Agent(agent/目录),由HttpAgent经BANKING_AGENT_URL(默认http://localhost:8124/)连接。也就是说,该皮肤的 teach-flow 工具(offerWorkflowRecording、awaitDashboardDemonstration、saveLearnedWorkflow,以及recall_memory/save_memory)是在远端 Agent 侧编排的;源码注释中明确测量过:跑这个 Agent 时,浏览器的前端工具、Intelligence MCP 工具、a2ui 中间件和 HITL 工具调用往返都能照常绑定。因此本文关于 FRAMING 的讨论在概念上对应这套远端提示词。
皮肤侧的工具注册在 src/skins/banking/tools.tsx:offerWorkflowRecording(向用户提议录制工作流)、awaitDashboardDemonstration(渲染“正在录制你的工作流”的实时卡片,描述里明确写着“不要给用户逐步指引、不要告诉他们点哪里——你不知道这个过程,这正是‘看着学’的意义所在”)、saveLearnedWorkflow(总结演示流程并请求保存)。
角色 5:KNOWLEDGE BACKEND —— save → recall → 新 Agent 学会
演示的流程被保存到持久化记忆;一个新 Agent 把它召回并成功无辅助完成。运行时是env-gated(按环境变量开关):默认使用 OSS 的InMemoryAgentRunner,配置了就使用CopilotKitIntelligence。
不变量:后端是一条可替换的接缝(swappable seam)。角色 #1–#2 不需要它就能被证明。OSS 模式下回路只在单次对话内有效;持久的跨线程 / 跨用户召回需要 Intelligence 模式(
recall_memory/save_memory工具从启用了记忆的后端挂载上来)。
不变量(先召回再拒绝:recall before declining):提示词必须让 Agent 在每次拒绝时先
recall_memory,再根据召回结果分支。一个把“你没有保存的通过办法”当作既定事实写死的提示词,会让 Agent 在已经被教过之后仍然拒绝并提出录制:演示成功、记忆正确保存,但收益永远不会出现——提示词压倒了 Agent 已经知道的东西。
运行时装配在 src/app/api/copilotkit/[[...slug]]/route.ts:
const intelligenceApiUrl = process.env.INTELLIGENCE_API_URL; const intelligenceWsUrl = process.env.INTELLIGENCE_GATEWAY_WS_URL; const intelligenceApiKey = process.env.CPK_INTELLIGENCE_API_KEY; const intelligenceEnabled = Boolean( intelligenceApiUrl && intelligenceWsUrl && intelligenceApiKey, );三个环境变量同时存在时走CopilotKitIntelligence(enableEnterpriseLearning: true通过 MCP 中间件把平台侧recall_memory/save_memory挂到本地 Agent 上,exposeMemoryRoutes: true打开面向客户端的/memories/*代理路由以便 web-inspector 的 Memory 标签页可用);任意一个缺失就回退到纯 SSE 的CopilotRuntime+InMemoryAgentRunner——这是默认路径,不允许回归。identifyUser回调按请求中的agentId(/agent/:agentId/run|suggest|connect路径或/threads?agentId=查询串)解析各皮肤的identifyUser,从而把记忆按成员/角色做作用域隔离;无 agentId 的应用级路由(/memories/*、/info)走defaultSkinId皮肤的身份解析器。
Per-skin divergence —— 记忆作用域(memory scope)
banking把学到的流程保存在scope:"project";后来的皮肤保存在scope:"user"并在提示词里说明,因为本次部署中一个 Intelligence 后端被所有产品共享:project 作用域的过程对从未学过它的皮肤也是可见的。新皮肤默认优先选"user",除非它确实独占自己的后端。
还有第二个更硬核的原因:一个皮肤的 intelligence/forget-memories.ts 如果跳过 project 作用域的行(所有带这个文件的皮肤都这么做,这样任何皮肤的 reset 都不会删掉另一个皮肤已种下的流程,可用grep -n project src/skins/*/intelligence/forget-memories.ts核对),那么它的 presenter reset物理上无法“解除教学”一条 project 作用域的记忆。在这种皮肤里把 beat 6 的流程存成 project 作用域,第二次跑 Demo 时 Agent 开局就知道答案:它不会拒绝、不会提出录制,而这个 beat 看似完美实则什么都没证明。选作用域之前,先查清楚自己皮肤的 sweep 到底会删掉什么。让 beat 5 种下的流程与 beat 6 学到的流程可区分,是它们文案和路由提示词的任务(每一条都明确说自己不是另一条),而不是scope字段的任务。
Per-skin divergence —— 工具命名
这条链的形状是固定的(offer → wait → summarize → confirm → persist),名字不是。banking把中间两个叫awaitDashboardDemonstration/saveLearnedWorkflow;后来的皮肤叫awaitDemonstration/saveLearnedProcedure。匹配你自己的皮肤,而不是本文——并且注意开头的 grep 以offerWorkflowRecording为键,这是所有皮肤共用的。
各角色在 banking 皮肤中的落点一览
| 角色 | 文件 |
|---|---|
| #1 GATE | src/app/api/banking/v1/transactions/[id]/route.ts —— 当审批会超出政策限额且未链接获批 exception 时,PUT返回422OVER_POLICY_LIMIT;规则辅助函数在 src/skins/banking/data/store.ts |
| #2 UNLOCK | 目录 src/skins/banking/data/policy-exception-codes.ts(POLICY_EXCEPTION_CODES、JUSTIFYING_EXCEPTION_CODES、isValidExceptionCode、isJustifying);REST src/app/api/banking/v1/exceptions/route.ts(开放,POST)+ src/app/api/banking/v1/exceptions/[id]/finalize/route.ts(finalize,POST) |
| #3 RECORDING | 壳层所有,非皮肤所有—— src/shell/teach/(RecordingProvider、useRecording、RecordingFeed、RecordingVignette;光晕 CSS 是 src/app/globals.css 的.recording-vignette)。只有logStep的 LABEL 是皮肤自己的 |
| #4 AGENT FRAMING | src/skins/banking/agent.ts —— 提示词不给配方、携带三个干扰工具、带 ACTION DISCIPLINE 条款,并定义 teach-flow HITL 工具:offerWorkflowRecording→awaitDashboardDemonstration→saveLearnedWorkflow,外加recall_memory/save_memory |
| #5 KNOWLEDGE BACKEND | src/app/api/copilotkit/[[...slug]]/route.ts —— env-gatedCopilotKitIntelligence(OSSInMemoryAgentRunner默认),键是INTELLIGENCE_API_URL/INTELLIGENCE_GATEWAY_WS_URL/CPK_INTELLIGENCE_API_KEY;enableEnterpriseLearning+exposeMemoryRoutes接好记忆工具与 inspector 的 Memory 标签页;identifyUser按成员/角色划分记忆作用域 |
叙述版流程:当被要求审批一笔它没有保存过流程的超限收费时,Agent 会拒绝(“我还没有保存过审批超限收费的办法”)并调用offerWorkflowRecording—— 不展示任何审批卡片。职员在真实仪表盘上演示(Transactions → Pending → 提交 justifying exception → approve),期间awaitDashboardDemonstration维持对话;随后 Agent 执行saveLearnedWorkflow+save_memory;之后的请求里,它通过openPolicyException→finalizePolicyException→approveTransaction把这套流程应用到另一笔超限收费上。因为演示发生在另一个路由上,teach/recall 工具由皮肤的Tools组件注册(而不是某个单页),这样它们能在导航后存活。
用命令而非“名单”来判定皮肤是否实现 Teach Mode
原文档特意不把“哪些皮肤实现了 teach mode”写进正文——写进文字里的名单正是这个仓库反复踩的坑(src/shell/skin-roster-docs.test.ts就是因为一次 review 里发生过十六次而存在的)。不变量是:一个皮肤实现了 teach mode,当且仅当它的Tools注册了录制交接(recording hand-off)。所以名单是命令,不是句子:
grep -l offerWorkflowRecording src/skins/*/tools.tsx这条 grep 现在已经返回每一个已注册的皮肤(用ls src/skins/对比),teach mode 不再是子集功能。但grep 给你名单,并不认证合规:不要把它读成“名单上的每个皮肤都遵守全部 5 个角色”——本文档一个更早的版本正是在只看两个皮肤的情况下这么写的,第三个皮肤立刻就证伪了它。角色 #3 的机械判定标准是:皮肤是否把它的两条回放规则从渲染里抽出来放进teach-mode-directives.ts,这同样是命令而非句子:
ls src/skins/*/teach-mode-directives.ts写作时逐角色核验的结果是:banking、commerce、logistics全部满足 5 个角色。people满足 #1、#2、#4、#5,而角色 #3 的两条回放不变量它满足一条、违反另一条——抄它之前两条都要读:
- “可回放(survives replay)”——满足。
people的awaitDemonstration渲染(src/skins/people/tools.tsx)在result里数\d+\.\s,而result是工具结果——即DemonstrationCard构建并交给respond?.()的“已观察步骤”指令(tools.tsx:1337-1341),编号也在其中。它不是在解析自己的渲染,那个分支也不会打印与数字矛盾的列表。指令才是回放的对象,所以计数是稳定的。但它仍然是脆弱的形态——它在解析一个计数而不是读取记录器上报的计数,所以步骤标签里如果出现数字会被算进去——但这是健壮性缺口,不是回放缺陷。 - “settle 不是回答”——违反。
saveLearnedProcedure的渲染(tools.tsx:1135-1139)对任何字符串结果都打印 “Saved. I'll use this next time”,而Don't save按钮就是用一个字符串 settle 的(tools.tsx:1164-1167)。卡片断言了一次从未发生的持久化写入——现场如此,每次回放也一模一样,这正是它能活下来的原因。
所以:要抄就抄回放行为被“钉住(pinned)”的皮肤——即上面teach-mode-directives.ts命令点名的那些。这些文件把两条规则从渲染中抽出来(readDemonstratedStepCount、classifySaveProcedureResult),让一个往返测试能同时拿住构造方与读取方。commerce和logistics是前两个;airline和keel在重构时也发了同一对,所以这种形态现在是常态而非例外。banking正确但手写、无断言,所以它可以腐烂而不会触发任何失败。这些被钉住的文件是**刻意的兄弟文件(siblings)**而非一个共享模块:皮肤对共享代码唯一的入向依赖是Skin契约,而这些指令是领域措辞,不是壳层机制。
下面两条 grep 都不是裁决——命中只是“值得去读的地方”,不是缺陷;空结果也不代表合规:
# SMELL,不是违规:一张 teach 卡片通过 PARSING 指令来推导计数, # 而不是读取记录器上报的计数。这是 people 的命中——但不是 people 的缺陷。 grep -n 'result\.match' src/skins/*/tools.tsx # people 真正缺陷的位置:在 settle 的“存在与否”上分支。 # 在按钮不可能产生分歧的卡片上是合法的,所以每个命中都要读; # 在“要我记住这个吗?”卡片上,它会在 _Don't save_ 之后打印 "Saved"。 grep -n 'typeof result === "string"' src/skins/*/tools.tsx合规皮肤在承重细节上偏离 banking 的地方,本文都在内联的Per-skin divergence注记里点出来了。构建下一个皮肤之前,请读完本文并跑完这两条命令。
钉住回放:teach-mode-directives.ts究竟在钉什么
以 src/skins/commerce/teach-mode-directives.ts 为例,看“构造方-读取方不可漂移”是如何被文件结构与测试钉住的。文件头注释给出了两条规则的动机:卡片只陈述生产者上报的事实,绝不通过解析渲染来重新推导事实。
第一,演示指令上报步数,读取方只读上报值:
export function buildDemonstrationDirective({ steps, code }: { steps: string[]; code: string | null; }): string { const observed = steps .map((label, index) => `${index + 1}. ${label}`) .join("\n"); return ( `The user finished after ${steps.length} ${steps.length === 1 ? "step" : "steps"}. ` + `Observed steps:\n${observed || "(nothing captured)"}\n` + (code ? `The waiver code they used was ${code}.` : "No waiver code was captured.") ); } export function readDemonstratedStepCount(result: unknown): number | null { if (typeof result !== "string") return null; const match = /^The user finished after (\d+) steps?\./.exec(result.trim()); return match ? Number(match[1]) : null; }注释记录了那条历史 bug:卡片过去靠数/\d+\.\s/匹配来“重新推导”步数,结果像 “Marked down Aurora Throw to 1.50 under the floor” 或 “Filed waiver MARGIN-EXEC-OK at 12.5% margin” 这样的步骤标签会各贡献一个幻影步数,卡片报的数字比下面列表印的多——而这恰恰是“录制是忠实的”这个论断所在的那个 beat。正则锚定在^开头,也是为了让自由文本步骤标签里的数字不可能被误当成计数。
第二,保存分类器绝不把“未知的 settle”当成成功:
export type SaveProcedureOutcome = "pending" | "saved" | "declined" | "unknown"; export function classifySaveProcedureResult(result: unknown): SaveProcedureOutcome { if (typeof result !== "string") return "pending"; const text = result.trim(); if (text.length === 0) return "pending"; if (text === SAVE_PROCEDURE_CONFIRMED) return "saved"; if (text === SAVE_PROCEDURE_DECLINED) return "declined"; if (/declined|do not call save_memory/i.test(text)) return "declined"; if (/confirmed/i.test(text) && /save_memory/i.test(text)) return "saved"; return "unknown"; }两个 settle 指令本身就是字符串常量(SAVE_PROCEDURE_CONFIRMED指示“用户确认,立即用 save_memory 持久化”;SAVE_PROCEDURE_DECLINED指示“用户拒绝,不要调用 save_memory”),与分类器同文件共存,“改一处就看得见另一处”。文件的往返测试(teach-mode-directives.test.ts)把构造方与读取方同时钉住,防止它们漂移。模块刻意保持 server-safe 且无 React,这样这些读取器值得单测且无需挂载 provider/线程/agent 注册。
验证
不依赖后端的证明(今天就能跑)—— 角色 #1 + #2
运行仓库自带的脚本,对着一个运行中的 dev server。它驱动真实 REST 路由,在 HTTP 层断言完整的 gate→unlock 契约,不需要任何 Intelligence 栈:
pnpm dev # 另开一个终端(默认 :3000) ./verify-teachable-gate.sh # BASE_URL 默认为 http://localhost:3000 BASE_URL=http://localhost:3000 ./verify-teachable-gate.sh它按顺序断言:
- A. GATE(#1)—— 审批一笔超限收费 →422
OVER_POLICY_LIMIT,且响应体不提及exception/unlock 路径(只描述症状)。 - B. UNLOCK(#2)—— 打开一个 justifying exception → finalize → 重新审批同一笔收费 →201(gate 已解除)。
- C. DECOY(#2)—— 非 justifying 代码能提交并 finalize,但审批仍然是422。
- D. CATALOGUE(#2)—— 非法代码 →422
INVALID_EXCEPTION_CODE,且不枚举真实目录。
存储是内存态的,从 src/skins/banking/data/seed.json 播种。每个场景用一笔不同的种子超限交易,所以一次运行不需要重置。想从零重跑,重启 dev server,或者
POST皮肤的 gated presenter-reset 路由(/api/banking/v1/dev/reset,由PRESENTER_RESET_ENABLED=true启用;侧边栏的 Reset 按钮也打这条路由)。学习证明之前优先用 reset 路由。重启 dev server 只会重新播种内存存储,其它什么都不做——持久化记忆活在 Intelligence 后端里,重启杀不掉它。reset 路由还会遗忘并重新播种那段记忆,这才是唯一能让 Demo 回到真正“未教过”状态的东西。它会在那半程失败时上报
memoryError;这句话是“回路开局就已学过”的唯一警告,所以要把它展示出来,而不是把响应压成一个状态码。
新 Agent 学习证明 —— 角色 #3 + #5(Intelligence 模式)
这一步证明回路学到了,而不只是 REST 能用。它需要配置好 env-gated 的CopilotKitIntelligence运行时(INTELLIGENCE_API_URL、INTELLIGENCE_GATEWAY_WS_URL、CPK_INTELLIGENCE_API_KEY)。
- 基线(Baseline)。先 reset(见上)——持久化记忆比 dev server 重启活得久,不 reset 的话开局就已经是“已学过”,对照组会空转(vacuously pass)。然后在一个新线程里让 Agent 审批一笔超限收费。在角色 #4 的 framing 完好时,它会拒绝并主动提出录制——不会发射任何干扰工具。这是对照组。
- 教学(Teach)。人类在仪表盘上演示解锁(justifying code → finalize → approve),记录器卡片逐条播报每个步骤。
- 保存(Save)。Agent 总结并调用
save_memory(kind:"operational";scope:"project"是 banking,"user"是后来的皮肤——见角色 #5)。 - 新 Agent 成功(Fresh agent succeeds)。在一个新线程里(对人类的会话没有任何记忆),请求审批另一笔超限收费。Agent
recall_memory→ 提交 justifying exception → finalize → approve——无辅助,提示词里什么都没加。
通过标准:第 1 步拒绝,第 4 步成功,且唯一的变化就是保存下来的流程。这个差值就是学习。它的确定性 CI 版本是pnpm test:self-learning(即 e2e/memory-learning.spec.ts,脚本定义于 package.json),用 aimock 提供的 LLM 驱动该流程,对着真实的本地记忆后端跑。
结语:把“演示-学习”变成可证明的契约
Teach Mode 的价值不在于“我们调了提示词让 Agent 看起来会了”,而在于它把学习做成了一条可逐角色验证的契约:gate 的错误只描述症状、unlock 的流程有区分度、recording 把事实装进工具结果而非渲染、framing 不给配方、后端是一条可换的接缝。grep -l offerWorkflowRecording src/skins/*/tools.tsx给你名单,ls src/skins/*/teach-mode-directives.ts给你回放行为被钉住的候选,而./verify-teachable-gate.sh让你在没有 Intelligence 后端时就能证明前半程,pnpm test:self-learning让你在 CI 里证明后半程。复制一套时,抄回放行为被钉住的皮肤(commerce/logistics/airline/keel的同款形态),记住“settle 不是回答”、演示步数要读上报值而不是数出来的值,并且新皮肤默认把记忆存到scope:"user"。这样你的下一个皮肤才能像 banking 一样,让 Agent 从“看着一个人做一遍”变成“真正会了”。
【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考