1. 项目概述当白板工具遇上AI智能体最近在折腾AI智能体Agent开发的朋友可能都绕不开一个核心问题如何让AI理解并操作那些非文本、非结构化的复杂界面比如一个功能丰富的在线白板工具。这正是“Agents365-ai/excalidraw-skill”这个项目要啃下的硬骨头。简单来说它就是一个让AI智能体能够“看见”并“操作”Excalidraw一个开源的虚拟手绘风格白板工具的技能包。听起来可能有点抽象我打个比方。传统的AI对话就像是你和一位盲人助手在交流你只能用语言描述“在左上角画个方框里面写上‘开始’”助手完全不知道“左上角”在哪“方框”长什么样。而有了这个技能相当于给这位助手装上了一双“眼睛”和一双“手”。它能直接“看到”白板画布上的所有元素图形、文字、箭头、位置并能通过编程接口精确地执行你的指令比如“把那个红色的圆形向右移动50像素”、“在现有两个矩形之间添加一条连接线”。这个技能的价值远不止是让AI多会用一个工具。它实际上是在为自动化工作流、智能UI测试、甚至是低代码/无代码的视觉化应用构建铺平了道路。想象一下你可以用自然语言指挥AI帮你绘制产品架构图、整理会议脑图草稿、或者自动检查UI设计稿是否符合规范。对于开发者、产品经理、设计师以及所有AI智能体研究者而言掌握如何让AI与图形界面交互是一个极具前瞻性和实用性的能力。接下来我就结合这个开源项目拆解一下实现这类“视觉-操作”型AI技能的核心思路、技术细节以及实操中会遇到的那些坑。2. 核心设计思路如何让AI“看懂”并“操作”一个白板要让AI操作Excalidraw我们不能指望AI像人一样去识别屏幕像素。核心思路是“结构化描述”加“API驱动”。整个设计可以分解为两个核心环节感知Perception和执行Execution。2.1 感知层设计将画布转化为AI可读的“场景描述”Excalidraw的画布是由一系列图形元素Element构成的每个元素都是一个JSON对象包含了其类型矩形、圆形、菱形、箭头、文本等、位置坐标、尺寸、样式填充色、边框色、粗细等属性。AI技能的第一步就是获取并解析这个元素列表。1. 状态抓取机制项目通常通过监听Excalidraw实例的状态变化或者直接调用其提供的getSceneElements()等API来实时获取当前画布上所有元素的JSON数据。这相当于给AI提供了一个实时的、结构化的“场景快照”。2. 场景描述生成直接给AI喂原始的JSON数组是低效且不友好的。我们需要一个“翻译”过程将结构化的数据转化为一段富含语义的自然语言描述。这个过程需要考虑空间关系描述元素之间的相对位置“A在B的左侧”“C被D包围”。层次与分组说明哪些元素是成组的是否有图层上下关系。属性摘要概括元素的视觉特征“一个蓝色的粗箭头”“一段居中的标题文字”。意图推断根据元素排列尝试描述可能的意图“这看起来像一个流程图的开端”。一个简单的描述生成器可能是这样的逻辑def generate_scene_description(elements): description f画布上共有 {len(elements)} 个元素。 for i, el in enumerate(elements[:5]): # 举例描述前几个元素 if el[type] rectangle: desc f一个位于({el[x]}, {el[y]})的矩形宽{el[width]}高{el[height]}填充色为{el[backgroundColor]}。 elif el[type] text: desc f一段文本内容为‘{el[text]}’字体大小为{el[fontSize]}。 # ... 处理其他类型 description f 元素{i1}: {desc} if len(elements) 5: description f 以及另外{len(elements)-5}个其他元素。 return description这样AI在决定如何操作前就能获得一个类似于“画布左上角有一个红色圆形其右侧20像素处有一个写着‘开始’的文本框下方连接着一个蓝色箭头指向一个矩形...”的认知。注意描述生成的质量直接影响到AI决策的准确性。过于简略会丢失关键信息过于冗长则会增加AI的理解负担和API调用成本。需要在信息密度和可读性之间做权衡有时还需要根据任务类型动态调整描述的重点例如绘图任务更关注位置和类型而美化任务更关注颜色和样式。2.2 执行层设计将自然语言指令映射为原子操作AI理解了场景后需要将用户的自然语言指令如“把那个圆形变大一点”转化为一系列对Excalidraw API的调用。这需要设计一套操作指令集。1. 原子操作定义这是技能的核心。我们需要定义一系列最基础、不可再分的操作例如create_element(type, properties): 创建新元素。update_element(element_id, updates): 更新现有元素的属性位置、大小、颜色、文字等。delete_element(element_id): 删除元素。group_elements(element_ids): 将多个元素编组。connect_elements(start_id, end_id, style): 在两个元素间创建连接线。每个原子操作都对应Excalidraw内部一个或多个具体的函数或状态更新动作。2. 指令解析与规划这是AI大语言模型LLM发挥作用的主战场。我们将当前的“场景描述”和用户的“自然语言指令”一同提交给LLM并要求它按照预定义的格式输出一个“操作计划”。这个计划就是一系列有序的原子操作。例如用户指令“在现有两个矩形中间画一个菱形并用箭头把它们从左到右连起来。” AILLM可能输出的操作计划1. identify_element: 定位“第一个矩形”假设ID为rect_1。 2. identify_element: 定位“第二个矩形”假设ID为rect_2。 3. calculate_position: 计算rect_1和rect_2的中间点坐标 (x_mid, y_mid)。 4. create_element: typediamond, xx_mid-30, yy_mid-30, width60, height60, backgroundColor#ddd。 5. identify_element: 定位新创建的菱形ID自动生成如diamond_3。 6. connect_elements: start_idrect_1, end_iddiamond_3, stylesolid。 7. connect_elements: start_iddiamond_3, end_idrect_2, stylesolid。3. 操作执行与反馈技能后端接收到这个JSON格式的操作计划后便按顺序调用对应的Excalidraw API执行。每执行一步都可以选择再次获取最新的画布状态并将其反馈给AI形成一个“感知-决策-执行-再感知”的闭环。这对于处理复杂、多步骤的指令至关重要可以确保上一步操作的结果符合预期再继续下一步。3. 关键技术实现细节拆解理解了宏观设计我们深入到代码层面看看几个关键部分如何实现。3.1 与Excalidraw实例的通信桥梁Excalidraw通常作为前端库嵌入在网页中。我们的AI技能后端可能是Node.js、Python服务需要与它通信。有几种常见模式1. 前端集成模式技能作为库将技能代码打包成一个JS库直接与网页中的Excalidraw实例运行在同一个浏览器上下文。这种方式能力最强可以直接调用Excalidraw的所有内部函数监听所有事件。excalidraw-skill项目很可能采用这种模式通过封装Excalidraw的exportToBackend和restore等API来同步状态并通过直接调用其updateScene等方法来操作元素。2. 后端驱动模式通过Puppeteer/Playwright在后端服务中通过无头浏览器如Puppeteer控制一个加载了Excalidraw的页面。后端通过CDPChrome DevTools Protocol向页面注入脚本执行操作并获取状态。这种方式将AI逻辑与前端解耦更适合自动化流水线但会引入无头浏览器的开销和复杂性。3. 混合模式前端负责状态采集和指令接收通过WebSocket将状态发送给后端AI服务后端返回操作计划前端再执行。这种模式平衡了能力和架构清晰度。在excalidraw-skill的语境下它很可能被设计为一种“技能包”供类似LangChain、AutoGPT这样的AI智能体框架调用。因此它需要提供标准化的接口如describe_scene()和execute_actions(actions)内部则采用前端集成模式来实现这些接口。3.2 元素定位与指代消解AI的“眼神儿”要好这是实操中最容易出错的环节。用户的指令充满了“这个”、“那个”、“左边的圆”、“标题文字”这样的指代。如何让AI准确地将这些指代与画布上的具体元素ID对应起来1. 属性匹配最直接的方法。当用户说“红色的圆”我们就在场景元素中查找type为ellipse且backgroundColor为红色或近似红色的元素。但问题很多颜色描述不精确“浅蓝”是什么RGB值、多个元素符合条件有两个红圆、属性组合复杂“那个带粗边框的文本框”。2. 空间关系推理这是提升准确性的关键。我们需要在场景描述中预先计算并加入元素间的空间关系。例如为每个元素计算其中心点然后判断与其他元素的相对位置左、右、上、下、最近。当用户说“最左边的矩形”我们就能根据计算筛选出来。3. 交互历史与上下文维护一个短暂的对话上下文。如果用户上一条指令是“创建一个圆形”那么下一条指令“把它填成红色”中的“它”就可以从上文创建的元素ID中引用。这需要技能在状态中记录最近的操作结果。4. LLM辅助解析将当前场景描述和模糊的用户指令一起交给LLM直接要求它输出一个或多个用于定位的筛选条件。例如 用户输入“修改第二个步骤框的颜色。” LLM输出{ target_filters: [ {type: rectangle}, {text_contains: 步骤}, {order_by: x_asc, index: 1} // 按x坐标排序取第二个索引1 ] }然后技能代码根据这些筛选条件在元素列表中查找。这种方式更灵活但依赖LLM的理解能力且可能不稳定。实操心得在实际开发中我通常会采用“混合定位策略”。首先尝试用明确的属性如ID、确切的文本内容进行精确匹配。失败后使用空间关系如“下方最近的那个”进行筛选。如果还是模糊则利用LLM从指令和场景描述中提取关键属性进行匹配。同时一定要将定位的结果匹配到的元素ID及其匹配原因作为日志输出或反馈给用户这非常有助于调试AI的“决策过程”。3.3 操作的安全性与事务性让AI直接操作生产环境的数据是有风险的。一个错误的循环操作可能导致画布被清空。因此安全设计必不可少。1. 操作验证与沙盒在执行任何修改操作前对操作参数进行验证。例如检查新的坐标是否在合理的画布范围内字体大小是否为正数。对于高风险操作如批量删除可以要求二次确认或设置权限开关。在开发阶段强烈建议在沙盒环境如一个独立的、无重要数据的Excalidraw实例中进行测试。2. 操作撤销/重做栈的维护Excalidraw本身有撤销/重做功能。但AI的复合操作一个指令对应多个原子操作需要被当作一个逻辑单元。理想情况下技能应该能管理自己的操作栈确保一次用户指令对应的所有原子操作可以作为一个整体被撤销。这可以通过在开始一组操作前设置一个“检查点”或在操作后生成一个逆操作集来实现。3. 错误处理与回滚在执行一串原子操作时如果中间某一步失败例如要更新的元素不存在了需要有明确的错误处理策略。是中止整个计划并报错还是尝试跳过继续执行更稳健的做法是支持事务性回滚即一旦失败自动撤销本指令内已执行的所有操作将画布恢复到指令开始前的状态。4. 集成与实战将技能嵌入AI智能体工作流单独一个Excalidraw技能无法工作它需要被集成到一个更大的AI智能体系统中。这里以集成到LangChain框架为例展示一个典型的工作流。4.1 构建LangChain Tool在LangChain中一个技能通常被封装成一个Tool对象。我们需要定义这个Tool的name,description和_run方法。from langchain.tools import BaseTool from typing import Type, Optional from pydantic import BaseModel, Field class ExcalidrawSkillInput(BaseModel): 输入模型定义AI调用此工具时需要提供的参数。 instruction: str Field(description用户对Excalidraw画布的具体操作指令例如‘画一个红色的圆’或‘把标题加粗’。) current_state_summary: Optional[str] Field(defaultNone, description当前画布状态的简要文字描述用于辅助AI理解上下文。) class ExcalidrawSkillTool(BaseTool): name excalidraw_editor description 用于操作和编辑Excalidraw白板画布。你可以创建图形、修改属性、添加文字、连接元素等。 args_schema: Type[BaseModel] ExcalidrawSkillInput def _run(self, instruction: str, current_state_summary: Optional[str] None) - str: 工具的核心执行逻辑。 1. 获取当前画布状态元素列表。 2. 结合状态和指令调用LLM生成操作计划。 3. 执行操作计划。 4. 返回执行结果或新的状态描述。 # 1. 获取当前场景元素 scene_elements self._get_current_scene() # 调用技能内部方法与Excalidraw实例交互 # 2. 生成场景描述可以缓存优化 scene_description self._generate_description(scene_elements) # 3. 调用LLM进行规划这里需要构造特定的Prompt llm_plan self._call_llm_for_plan(scene_description, instruction) # 4. 解析并执行LLM返回的操作计划 execution_result self._execute_plan(llm_plan, scene_elements) # 5. 获取执行后的新状态并生成反馈 new_scene_elements self._get_current_scene() feedback self._generate_feedback(execution_result, new_scene_elements) return feedback def _call_llm_for_plan(self, description: str, instruction: str) - dict: # 构造一个精心设计的Prompt引导LLM输出结构化的操作计划 prompt_template 你是一个控制Excalidraw画布的助手。当前画布状态描述如下 {scene_description} 用户请求{user_instruction} 请根据以上信息生成一个JSON格式的操作计划。你可以使用的原子操作有 - create_element(type, x, y, width, height, ...) - update_element(id, {property: new_value}) - delete_element(id) - group_elements([id1, id2]) - connect_elements(start_id, end_id) 请只输出JSON不要有其他解释。 prompt prompt_template.format(scene_descriptiondescription, user_instructioninstruction) # 调用LLM API例如OpenAI GPT-4并指定返回格式为JSON response openai_chat_completion(prompt, temperature0.1, response_format{ type: json_object }) import json return json.loads(response[choices][0][message][content]) # ... 其他内部方法 _get_current_scene, _execute_plan 等的实现4.2 设计高效的Agent Prompt将Tool交给Agent后如何让Agent学会在合适的时机调用它这依赖于系统提示词System Prompt的设计。一个有效的System Prompt需要明确角色与能力告诉AI它现在拥有了操作Excalidraw的能力。工具说明清晰说明excalidraw_editor工具的作用、输入参数instruction和可选的current_state_summary的含义。调用策略指导AI何时该使用这个工具。例如“当用户的要求涉及修改、创建、删除Excalidraw画布上的内容时你必须使用excalidraw_editor工具。在调用工具前你应该先简要总结当前画布的关键信息作为current_state_summary参数以帮助工具更准确地理解上下文。”输出期望要求AI在工具执行后向用户解释它做了什么。4.3 实战流程示例假设我们已经设置好了一个集成了ExcalidrawSkillTool的LangChain Agent。一次完整的交互可能如下用户“帮我在画布中央画一个流程图第一步是‘需求分析’用菱形表示。”AI Agent思考理解到这是一个Excalidraw操作请求。它可能先调用工具获取当前画布状态发现是空的。然后它构造指令“在画布中央创建一个菱形内部添加文字‘需求分析’。”调用excalidraw_editor工具传入该指令。工具执行获取空画布状态。生成描述“画布为空。”LLM规划[create_element(type: diamond, x: 400, y: 300, ...), update_element(id: new_diamond_id, text: 需求分析)]执行操作成功。返回反馈“已成功在画布中央创建了一个包含文字‘需求分析’的菱形。”AI Agent回复用户“好的我已经在画布中央为您创建了一个代表‘需求分析’的菱形步骤。接下来需要添加其他步骤吗”5. 常见问题、调试技巧与优化方向在实际开发和测试这类AI图形操作技能时你会遇到一系列典型问题。下面是我踩过坑后总结的一些排查思路和优化建议。5.1 典型问题与排查清单问题现象可能原因排查步骤与解决方案AI完全无视工具用文字描述回答。1. System Prompt中工具描述不清晰或未强调必要性。2. 工具description字段不够吸引AI调用。3. LLM温度temperature设置过高导致行为随机。1. 强化System Prompt“必须使用工具来处理画布操作。”2. 优化工具描述以动词开头如“使用此工具来绘制、移动、编辑Excalidraw元素...”。3. 将LLM调用温度调低如0.1增加确定性。AI调用了工具但指令instruction参数传递得模糊不清。AI没有理解需要将用户语言转化为精确的操作描述。在System Prompt中提供调用示例“例如用户说‘画个红圈’你应该调用工具并设置instruction‘创建一个红色的圆形’。”工具执行错误如“元素未找到”。指代消解失败。AI在操作计划中引用了不存在的元素ID。1.增强日志在工具_run方法中打印出接收到的instruction、生成的scene_description和LLM返回的plan。这是最重要的调试手段。2.改进场景描述在描述中加入每个元素的唯一标识如el_1,el_2和更精确的空间关系。3.让AI输出筛选条件而非具体ID修改Prompt要求LLM输出定位元素的属性条件由工具代码执行查找。操作结果不符合预期位置不对、样式错了。1. LLM对坐标、尺寸等数字计算不准。2. Excalidraw的坐标系统原点在左上角与AI理解有偏差。3. 颜色等样式名称映射错误。1.减少LLM的数字计算负担在Prompt中明确坐标系或让AI输出相对描述“向右移动一些”由工具代码转换为具体像素值。2.提供样式常量映射表在Prompt中给出{“红色”: “#ff0000”, “蓝色”: “#0000ff”, “粗”: “bold”}让AI使用这些标准值。3.分步执行与确认对于复杂操作让AI先输出计划经用户或系统确认后再执行。性能慢响应延迟高。1. 每次调用都生成完整的场景描述文本过长。2. 与Excalidraw实例通信频繁。1.增量描述只描述相对于上次有变化的部分或只描述画布中一个局部区域。2.操作批处理让LLM一次性规划多个操作减少来回通信次数。3.缓存缓存画布元素数据只有在确实发生变化时才更新。5.2 高级优化方向当基础功能跑通后可以考虑以下方向提升技能的智能性和鲁棒性1. 多模态能力增强目前的“感知”依赖于代码生成的结构化描述。未来可以结合视觉模型VLM让AI直接分析画布的截图。这样能捕捉到更丰富的视觉信息比如手绘风格的不精确对齐、自由绘制路径的形态等这些是纯数据结构难以描述的。可以设计一个流程先由VLM生成一段视觉描述再结合数据结构形成更全面的“场景理解”。2. 复杂意图的理解与分解用户可能会提出非常高层级的目标如“把这个架构图美化一下”。这需要技能具备任务分解能力。可以引入一个专门的“规划器”LLM先将模糊目标分解为“调整布局使其对称”、“统一颜色主题”、“添加图标装饰”等子任务再针对每个子任务调用具体的操作技能。3. 技能的记忆与学习让技能能够从历史交互中学习。例如如果用户多次将“重要节点”标为橙色那么当用户再次说“高亮关键部分”时AI可以优先选择橙色。这可以通过维护一个与用户或会话相关的偏好配置文件来实现。4. 与工作流引擎集成将Excalidraw技能作为自动化工作流的一个节点。例如可以从Notion读取项目清单自动生成任务看板图或者将画好的架构图导出自动触发部署脚本。这需要技能提供更丰富的API如导入/导出特定格式的数据、监听特定事件等。开发像excalidraw-skill这样的AI技能本质上是在为AI构建通往物理世界和数字世界各类工具的“手”和“眼”。这个过程充满挑战从精确的元素定位到安全的操作执行每一个环节都需要细致的工程化思考。但它的回报也是巨大的一旦跑通你将解锁一系列自动化与智能交互的新场景。我个人的体会是从最简单的“画个圆”开始逐步迭代处理好错误边界并始终保持详尽的日志记录是成功构建此类复杂技能的不二法门。