AI智能体如何操作图形界面:以Excalidraw白板为例的工程实践
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" 元素{i+1}: {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,并要求它按照预定义的格式输出一个“操作计划”。这个计划就是一系列有序的原子操作。
例如,用户指令:“在现有两个矩形中间画一个菱形,并用箭头把它们从左到右连起来。” AI(LLM)可能输出的操作计划:
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: type="diamond", x=x_mid-30, y=y_mid-30, width=60, height=60, backgroundColor="#ddd"。 5. identify_element: 定位新创建的菱形(ID自动生成,如diamond_3)。 6. connect_elements: start_id=rect_1, end_id=diamond_3, style="solid"。 7. connect_elements: start_id=diamond_3, end_id=rect_2, style="solid"。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的页面。后端通过CDP(Chrome 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(default=None, 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_description=description, user_instruction=instruction) # 调用LLM API,例如OpenAI GPT-4,并指定返回格式为JSON response = openai_chat_completion(prompt, temperature=0.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构建通往物理世界和数字世界各类工具的“手”和“眼”。这个过程充满挑战,从精确的元素定位到安全的操作执行,每一个环节都需要细致的工程化思考。但它的回报也是巨大的,一旦跑通,你将解锁一系列自动化与智能交互的新场景。我个人的体会是,从最简单的“画个圆”开始,逐步迭代,处理好错误边界,并始终保持详尽的日志记录,是成功构建此类复杂技能的不二法门。
