Show HN: Agent Draw:在你说话时由AI代理绘画,基于 TLDraw 构建

Hacker News Top 工具

摘要

Agent Draw 是一个开源工具,允许AI代理在你演示时在 TLDraw 画布上绘图,基于 TLDraw 的 agent starter kit 构建。它支持排队绘图请求,并测试不同的模型,如 Claude Opus 和 Haiku。

我们最近在 tldraw(一个用于 React 的无限画布 SDK)上构建了 2draw,这是一款类似 Drawful 的游戏,玩家在共享画布上绘画并竞猜彼此的画作。<p>我们开始思考,如果将代理引入其中作为对手或竞争猜图者会怎样,并深入研究了代理如何读取和绘制 tldraw 画布。这项研究最终演变成了:<p>Agent Draw,一个让你在演示时让代理在画布上为你绘图的工具。<p>你现在就可以试用,或获取源代码:<p>- 在线演示:<a href="https://tldraw-agent-draw-demo.james-664.workers.dev" rel="nofollow">https://tldraw-agent-draw-demo.james-664.workers.dev</a><p>- 源代码:<a href="https://github.com/ritza-co/tldraw-agent-draw-demo" rel="nofollow">https://github.com/ritza-co/tldraw-agent-draw-demo</a><p>这里你可以看到代理在我为三年级化学课做演示时协助我:<p>- <a href="https://www.youtube.com/watch?v=iIH2hJAxxm8" rel="nofollow">https://www.youtube.com/watch?v=iIH2hJAxxm8</a>
查看原文
查看缓存全文

缓存时间: 2026/07/12 22:51

# Agent Draw:在你说话时由代理自动绘图,基于 TLDraw 构建 来源:https://techstackups.com/articles/tldraw-agent-draw/ 我们最近基于 tldraw(https://tldraw.dev/)(一个用于 React 的无限画布 SDK)构建了 2draw(https://2draw.ritzademo.com/#pub-4po650),这是一款类似 Drawful 的游戏,玩家在共享画布上绘画并竞猜彼此的画作。我们开始思考,如果在该循环中加入一个代理作为对手或竞猜者会怎样,并深入研究了代理如何读取并在 tldraw 画布上绘图。这项研究最终演变为:**Agent Draw**,一个在你演示时让代理为你绘制画布内容的工具。下图展示了代理在我为三年级化学课做的演示中协助我的场景。 你可以立即尝试,或获取源代码: - **在线演示:** tldraw-agent-draw-demo.james-664.workers.dev(https://tldraw-agent-draw-demo.james-664.workers.dev/) - **源代码:** github.com/ritza-co/tldraw-agent-draw-demo(https://github.com/ritza-co/tldraw-agent-draw-demo) ## Agent Draw 是一个在你演示时绘图的代理 (https://techstackups.com/articles/tldraw-agent-draw/#agent-draw-is-an-agent-that-draws-while-you-present) 在画布上拖出一个矩形,说出你想放在矩形内的内容,当你回头再看时,它已经画好了——由 AI 代理在你继续说话时绘制完成。连续拖出几个矩形,它们会排队依次绘制。所有这些都发生在一个名为 tldraw 的无限画布工具上。 ## 什么是 tldraw? (https://techstackups.com/articles/tldraw-agent-draw/#what-is-tldraw) tldraw(https://tldraw.dev/)是一个用于 React 的无限画布 SDK:用户用鼠标驱动相同的编辑器 API,而代理则可以通过代码驱动,创建形状、移动形状、在形状之间绘制箭头。我们的代理并非从零构建。tldraw 已经发布了官方的 Agent 入门套件(https://github.com/tldraw/agent-template),它通过聊天面板绘制和排列形状,后端由 Cloudflare Worker 支持。我们的 Agent Draw 就是在此基础上构建的。 ## 代理的绘画能力有多强? (https://techstackups.com/articles/tldraw-agent-draw/#how-good-is-an-agent-at-drawing) 基于 tldraw 基本元素(矩形、菱形、箭头)构建的简单构图,大多数模型都能很好地处理。更智能的模型往往在构图上更具野心,并且在画布上的放置位置更准确。我们通过在同一会话中给每个模型两个相同请求来测试这一点:绘制一个决策图,以及绘制一个打板球的人。 `claude-opus-4.8` 对两个请求都处理得很好:一个由基本元素构建的清晰决策图,以及更有趣的是,一个用钢笔工具绘制的完整板球场景: claude-opus-4.8 的写意钢笔素描:一个火柴人击球手挥棒击打板球,带有三柱门和 wicket,完全使用钢笔工具而非几何基本元素绘制 ### 不足之处 (https://techstackups.com/articles/tldraw-agent-draw/#where-it-struggles) 那是 `claude-opus-4.8`,是目前能力较强的模型之一。如果用较小的模型处理相同的请求,结果会大不相同。 #### 较小模型会降低追求 (https://techstackups.com/articles/tldraw-agent-draw/#a-smaller-model-settles-for-less) 将相同的两个请求交给较小的模型,其追求会迅速下降。`claude-haiku-4.5` 在决策图上与 Opus 相当,但面对板球请求时,它只使用基本元素而不使用钢笔,并满足于一个带有标签的更简单、静态的构图,而非动态素描: claude-haiku-4.5 的板球运动员:由简单基本元素(身体、球棒和球的矩形和圆形)加上文本标签构成,构图远不如 Opus 的写意素描有野心 #### 较弱的模型可能会放弃任务 (https://techstackups.com/articles/tldraw-agent-draw/#a-weaker-model-can-give-up-on-the-task) 再降低到 `google/gemini-2.5-flash-lite`,它似乎对两个请求都早早放弃了: google/gemini-2.5-flash-lite 的不完整结果:一个半成品人物,手臂线条不连续,旁边是来自先前请求的未完成决策菱形 ## 我们如何制作 Agent Draw (https://techstackups.com/articles/tldraw-agent-draw/#how-we-made-agent-draw) 整个功能包括一个新画布工具、一个语音管道、一个序列化绘制队列和一个提示部分。以下是每个部分的实际代码,来自 仓库(https://github.com/ritza-co/tldraw-agent-draw-demo)。 ### 捕获用户绘制的区域 (https://techstackups.com/articles/tldraw-agent-draw/#capturing-the-region-you-draw) 使用 Agent 绘制工具拖出一个区域,在 tldraw 画布上显示虚线选择刷子 tldraw 工具是状态机。你继承 `StateNode` 并定义子状态;tldraw 将指针事件路由到当前活动的状态。我们的 `AreaCaptureTool` 有三个状态(`idle` → `pointing` → `dragging`),并在指针抬起(拖出的矩形最终确定时)执行实际工作: `` class AreaCaptureDragging extends StateNode { static override id = 'dragging' private bounds: BoxModel | undefined = undefined override onPointerUp() { this.editor.updateInstanceState({ brush: null }) if (!this.bounds) throw new Error('Bounds not set') // 将捕获的矩形(页面坐标)交给捕获会话。 startCaptureSession(this.bounds) this.parent.transition('idle') } updateBounds() { if (!this.initialPagePoint) return const currentPagePoint = this.editor.inputs.getCurrentPagePoint() const x = Math.min(this.initialPagePoint.x, currentPagePoint.x) const y = Math.min(this.initialPagePoint.y, currentPagePoint.y) const w = Math.abs(currentPagePoint.x - this.initialPagePoint.x) const h = Math.abs(currentPagePoint.y - this.initialPagePoint.y) // 拖动时显示 tldraw 原生的选择刷子。 this.editor.updateInstanceState({ brush: { x, y, w, h } }) this.bounds = { x, y, w, h } } } `` 我们通过写入 `editor.updateInstanceState({ brush })` 免费获得了实时选择刷子矩形,这与 tldraw 自身选择工具使用的实例状态相同。边界采用页面坐标,因此无论用户如何平移或缩放,都能保持正确。 ### 在你说话时监听 (https://techstackups.com/articles/tldraw-agent-draw/#listening-while-you-talk) 捕获一开始,我们就打开麦克风。`AreaRecorder` 是对浏览器 `MediaRecorder` 的轻量封装,故意不涉及代理或转录的知识,仅提供 `start()` 和 `stop()`: `` export class AreaRecorder { async start(): Promise<void> { this.stream = await navigator.mediaDevices.getUserMedia({ audio: true }) const recorder = new MediaRecorder(this.stream, { mimeType: this.mimeType }) this.chunks = [] recorder.ondataavailable = (event) => { if (event.data.size > 0) this.chunks.push(event.data) } recorder.start() this.recorder = recorder } async stop(): Promise<Blob> { // ...停止录制,释放麦克风,解析出录制的片段... return new Blob(this.chunks, { type: this.mimeType || 'audio/webm' }) } } `` ### 将语音转换为文本 (https://techstackups.com/articles/tldraw-agent-draw/#turning-speech-into-text) 音频 blob 被发送到同一 Cloudflare Worker(已服务于代理)上的一个新 `/transcribe` 路由。该路由仅将音频转发给 Mistral 的 Voxtral(https://docs.mistral.ai/capabilities/audio/) 转录模型并返回文本,因此服务器持有的密钥默认覆盖所有演示访客: `` export async function transcribe(request: IRequest, env: Environment) { const form = await request.formData() const file = form.get('file') // 访客自己的密钥(如果添加了)优先级高于服务器密钥。 const mistralKey = request.headers.get('x-mistral-api-key') || env.MISTRAL_API_KEY const outForm = new FormData() outForm.append('file', file) outForm.append('model', 'voxtral-mini-transcribe-2507') const mistralResponse = await fetch('https://api.mistral.ai/v1/audio/transcriptions', { method: 'POST', headers: { Authorization: `Bearer ${mistralKey}` }, body: outForm, }) const data = (await mistralResponse.json()) as { text?: string } return new Response(JSON.stringify({ text: data.text ?? '' }), { headers: { 'Content-Type': 'application/json' }, }) } `` ### 同时处理多个绘制请求 (https://techstackups.com/articles/tldraw-agent-draw/#handling-more-than-one-drawing-at-a-time) 画布上同时有多个捕获在进行,每个都有自己的状态标签(录制中、排队中、绘制中) 这是使多个捕获协同工作的部分。你可以在第一个图形仍在绘制时拖出第二个矩形,捕获会按顺序绘制,而不是争夺画布。整体是一个基于 tldraw 的 `atom` 的模块级状态机:一个活跃的录制器、一个 FIFO 队列,以及一个单消费者工作进程,一次处理一个会话。开始新捕获会自动停止仍在录制的会话并将其加入队列: `` export function startCaptureSession(bounds: BoxModel): string { // 绘制新捕获会结束仍在录制的音频。 if (recordingId) finalizeRecording(recordingId) const id = nextId() sessions.set([...sessions.get(), { id, bounds, status: 'recording' }]) const rec = new AreaRecorder() recorder = rec recordingId = id rec.start().catch(/* 在会话上显示麦克风权限错误 */) return id } `` 消费者一次从队列中取一个会话,逐个经历 `transcribing` → `drawing`。这里的序列化是关键:代理一次只运行一个请求,因此新捕获只需等待轮到它: `` async function processQueue(): Promise<void> { if (processing) return // 单消费者 processing = true try { while (queue.length > 0) { const id = queue.shift() as string const session = getSession(id) const blob = pendingBlobs.get(id) // ...如果被取消/无音频/代理未就绪则跳过... patchSession(id, { status: 'transcribing' }) const text = await transcribe(blob) // ...如果所选模型的提供者需要访客未提供的密钥,则在此报错... patchSession(id, { status: 'drawing' }) await requestDrawInArea(agentRef, text, session.bounds) removeSession(id) // 清除此捕获的叠加层 } } finally { processing = false } } `` 较早的版本尝试实时传输麦克风并在你说话时绘制。但延迟很严重,且价值模糊,所以我们改为这种离散的捕获-然后-说话模型,而队列则取代了旧版本中“两个捕获互相取消”的重叠 bug。 ### 让代理持续绘制直到任务完成 (https://techstackups.com/articles/tldraw-agent-draw/#making-the-agent-keep-drawing-until-the-job-is-done) 每个会话的转录文本和边界被传递给 `requestDrawInArea`。这里有趣的决策是,我们使用了入门套件的完整代理循环 `agent.prompt`,而不是单轮 `agent.request`。`prompt` 会自行持续轮次操作,直到模型没有更多内容要添加,因此模型在单个调用中完成整个绘制,而不是只画一个形状就停止。 `` export async function requestDrawInArea( agent: TldrawAgent, text: string, bounds: BoxModel ): Promise<number> { const area = { type: 'area' as const, bounds, source: 'user' as const } ensureMode(agent, 'working') const before = agent.editor.getCurrentPageShapeIds().size try { await agent.prompt({ message: buildAreaMessage(text), contextItems: [area], }) return agent.editor.getCurrentPageShapeIds().size - before } finally { ensureMode(agent, 'idling') } } `` 较早的版本手动实现了继续循环,并增加了额外的 lint 检查来强制完成绘制,因为单轮请求只会绘制一个形状。一旦我们确认 `agent.prompt` 能自行完成整个绘制,所有这些脚手架都被删除了。 ### 提高速度 (https://techstackups.com/articles/tldraw-agent-draw/#making-it-faster) 入门套件的 `working` 模式附带了一个相当广泛的动作列表。对于固定区域绘制,其中两个动作每次都会消耗一次完整的 LLM 往返,却没有改善结果。 `setMyView` 移动相机以框住代理刚绘制的形状。对于画布聊天面板来说这是合理的,但对于区域捕获来说,视口无关紧要——用户想看到画了什么,而不是相机到处跳。问题是 `setMyView` 会中断当前轮次并触发重新请求,增加一次往返。 `review` 运行入门套件的 lint 系统:它检查重叠形状、文本超出框等类似问题,然后安排后续轮次进行修复。同样,在聊天面板中有用,但对于一次性区域绘制来说,它只是在最后增加了另一次模型调用。 从 `working` 模式的动作列表中移除这两个动作后,每次捕获的模型调用次数大约减少了一半。改动在 `AgentModeDefinitions.ts` 中只有两行: `` // review 和 setMyView 已移除——每个都会在每次绘制时强制增加一次额外往返, // 且对固定区域结果无帮助。工具类仍为其他模式注册。 actions: [ ThinkActionUtil.type, CreateActionUtil.type, PenActionUtil.type, // ...其余动作... ] `` 工具类仍保持注册,因此聊天面板仍可使用。只是在代理在捕获区域内绘制时,不向它提供这些动作。 ### 教会模型绘图,而非转录 (https://techstackups.com/articles/tldraw-agent-draw/#teaching-the-model-to-draw-not-transcribe) 左侧:口语请求被转录为一段文字。右侧:同一请求被绘制成清晰的带标签图表,而非转录文本墙 最后一部分是提示。如果放任不管,给模型一段语音句子,它往往会把那句话写成画布上的一段文字。我们为入门套件的系统提示编写了“在捕获区域内绘图”部分,它做了两件事:强制模型评估哪种视觉形式最适合请求,并禁止逐字转录音频: `` 选择最适合请求的视觉形式,并立即发出 create/pen 动作——不要要求澄清,不要停下来思考而不绘图: - 特定命名的形状(“画一个红圈”、“一颗星”):精确绘制该形状。 - 单个物体或插图:画好它,而不是多框图表。 - 定义或解释:绘制带关键词标签的标注图,而不是将口语句子作为文本块。 - 图表或流程:带箭头的标注节点。 - 图表:用于定量或比较内容。 如果请求不明确,做出最佳解释并仍然绘制。 切勿停顿——至少发出一个形状。 `` 它还坚持模型在单轮中构建*完整*结果(“用户不会在动作之间回复,因此你推迟到后续消息的任何内容都不会发生”),这与上面 `agent.prompt` 的决策相辅相成。 ### 将工具添加到工具栏 (https://techstackups.com/articles/tldraw-agent-draw/#adding-the-tool-to-the-toolbar) ‘Agent draw’ 工具已添加到 tldraw 工具栏,与 tldraw 内置工具相邻,并带有自己的图标和键盘快捷键 最后,通过 tldraw 的标准覆盖 API 注册该工具,使“Agent draw”与内置工具一起出现在工具栏中,并拥有自己的图标和键盘快捷键: `` const tools = [AreaCaptureTool, TargetShapeTool, TargetAreaTool] const overrides: TLUiOverrides = { tools: (editor, tools) => ({ ...tools, 'area-capture': { id: 'area-capture', label: 'Agent draw', kbd: 'a', icon: agentDrawIcon, onSelect() { this.editor.setCurrentTool('area-capture') }, }, // ...Pick Area / Pick Shape 保持不变... }), } `` 这就是整个功能:一个捕获矩形的工具、一个录制器、一个用于转录的 Worker 路由、一个队列、一次进入代理循环的调用、为速度修剪的两个动作,以及一个提示部分。其他所有内容(画布、代理、动作系统、流式后端)都来自入门套件。 *许可说明:我们的代码采用 MIT 许可证(https://github.com/ritza-co/tldraw-agent-draw-demo/blob/main/LICENSE.md),与 tldraw 入门套件相同。*

相似文章

DayuanJiang/next-ai-draw-io

GitHub Trending (daily)

Next AI Draw.io 是一个开源的 Next.js 网页应用,它集成了 AI 能力与 draw.io,通过自然语言命令来创建和修改图表。该应用支持多个大型语言模型提供商,并提供在线演示。

@geekbb: 一套让 AI 画出"对且好看"的 draw 架构图的框架加命令行工具。 靠真实 stencil 校验和自动布局,覆盖 AWS/Azure/GCP/Databricks/BPMN,专门治 AI 瞎编图形 ID 画出空白框的毛病。 https…

X AI KOLs Timeline

A framework and CLI tool that enables AI agents to generate structurally precise and aesthetically standardized draw.io diagrams for AWS, Azure, GCP, Databricks, and BPMN architectures, using real stencil validation and automatic layout to prevent hallucinated empty shapes.