DeepTutor 题图还原指南:从 Prompt 设计到 GeoGebra 图形生成的单次视觉分析
【免费下载链接】DeepTutorDeepTutor: Lifelong Personalized Tutoring. https://deeptutor.info/.项目地址: https://gitcode.com/GitHub_Trending/dee/DeepTutor
导读:本文以 DeepTutor 仓库中视觉求解 Agent(Vision Solver Agent)的核心提示词 deeptutor/agents/vision_solver/prompts/geogebra.md 为骨架,系统讲解"数学题配图 → GeoGebra 可执行命令序列"的完整还原管线:包括图像权威性判断、"反假设"定点原则、GeoGebra 命令语法约定、结构化 JSON 输出协议,并结合该 Agent 的源码实现、内置工具注册与前端渲染器,帮助读者理解这一功能在 DeepTutor 的 chat 与 solve(解题)场景中如何真正落地,以及若要复现或改进同类"图转可交互图形"的 Agent,提示词应如何设计。
一、任务背景:什么是"题图 → GeoGebra 还原"?
在数学(尤其是平面几何)的个性化辅导场景中,一道题往往同时包含题干文字与配图。人类解题者会先在脑中重建图形,再分析其中的点、线、圆及其相互关系。DeepTutor 将这一过程交给视觉大模型自动完成:模型一次性"看懂"配图中的几何元素与约束,并直接输出一段可在 GeoGebra 中执行的命令序列,将图形精确还原成可交互、可拖拽的几何画板。
这一定位在 vision_solver_agent.py 的模块注释中写得很清楚:它把旧版"BBox → Analysis → GGBScript → Reflection"的四阶段管线,收敛为一次视觉调用——读图后直接产出 GeoGebra 命令,仅在首轮未产出任何可用命令时才触发一次"门控修复"重试。公开接口process+format_ggb_block保持不变,因此唯一的现存消费方——内置工具geogebra_analysis(同时被 chat 与 solve 使用)无需任何改动。
本文要剖析的 geogebra.md 正是驱动该视觉模型的核心提示词,它定义了从"看图"到"写命令"的全套决策规则与输出协议。
二、输入与角色设定:一次调用要同时完成两件事
提示词将模型角色设定为"几何与 GeoGebra 绘图专家",输入只有两部分:
{{ question_text }} ← 题目题干(由代码运行时替换注入) [用户上传的题目配图] ← 图片以多模态消息传给视觉模型在实现层面,vision_solver_agent.py 的_analyze方法通过prompt.replace("{{ question_text }}", question_text or "")完成题干注入,并构建 OpenAI 风格的content数组:一条text消息(提示词)+ 一条image_url消息(data:image/<fmt>;base64,…形式的图片)。_call_vision_llm使用temperature=0.3的低随机度,保证命令输出稳定、可复现,使用的模型优先取vision_model,缺省回落到会话配置的model。
注意此处要求模型"一次性完成"两件事:理解(图中的几何元素与约束)+生成(可直接执行的 GeoGebra 命令序列)。这既是 Prompt 设计理念——把复杂任务压缩为单轮视觉推理,减少多阶段调用的延迟与错误累积——也直接决定了后续 JSON 输出协议的结构。
三、第一步:判断图像权威性(image_is_reference)
图是否可信、是否可以作为几何关系的依据,取决于题干有没有"引用图"。提示词要求模型首先扫描题干中是否含有图像引用词,包括:图 / 如图所示 / 看图 / 从图中 / 图示 / 图中 / 根据图 / 观察图 / 参照图。
- 命中引用词 →
"image_is_reference": true:图是核心信息源,题干未明确定义的点与位置,以图中相对位置为准; - 未命中引用词 →
"image_is_reference": false:以题干文字为准,图仅供参考,不得把图中推测的相对关系当作事实写入。
这一判断被完整保留到结构化输出中。从 GeoGebraAnalysisTool.execute 可以看到,最终结果会把image_is_reference一并写入返回给上层 Agent 的ToolResult.metadata,供后续解题逻辑决定"图形的约束信息是否可信"。
四、第二步:定点(反假设原则)—— 本提示词最重要的设计
几何还原最容易出错的地方,不是命令语法,而是模型"看图脑补"几何关系。比如图里 C 点恰好位于 AB 中点附近,模型就可能无中生有地写成Midpoint。为此,提示词提出"反假设原则",把每个点强制归类为下列三选一,不允许出现第四种"看起来像":
| 类型 | 判定标准 | GeoGebra 写法 |
|---|---|---|
| 题干给坐标 | 题干明确写出坐标,如 "A(-3,0)" | A = (-3, 0) |
| 派生点 | 题干用文字明确定义其生成关系,如 "M 是 AB 中点""P 是 l 与 m 交点" | M = Midpoint[A, B]、P = Intersect[l, m] |
| 图中自由点 | 图中可见,但题干既未给坐标、也未文字定义其关系 | 直接看图估算坐标:C = (估算x, 估算y) |
绝对禁令:题干没说 C 是中点/交点,仅凭"看起来像"就写成Midpoint/Intersect。这类点一律按"图中自由点"看图估坐标。
关于坐标估算,提示词给出了实操方法:题干已给出若干点的坐标时,用它们作锚点,按图中相对位置比例估算自由点坐标。同时给出一个极容易被视觉模型忽略的坑——坐标系方向差异:图像坐标 y 轴向下,GeoGebra y 轴向上,估算纵坐标时要换算方向。最后强调"图中可见的点必须全部画出",避免还原出的图形残缺。
五、GeoGebra 命令参考:模型必须遵守的语法全集
提示词为视觉模型内置了一份浓缩版的 GeoGebra 命令手册,覆盖九大类:
点 / 向量
- 直角坐标点
A = (x, y);极坐标P = (5; 60°) Intersect[a, b](两对象交点)、Intersect[a, b, n](第 n 个交点)Midpoint[A, B]、Center[c]- 向量
v = (3, 4)、Vector[A, B]
线
Segment[A, B](线段)、Line[A, B](直线)、Ray[A, B](射线)- 直线方程:
g: y = 2x + 1或一般式g: 3x + 2y = 6 Perpendicular[A, line](垂线)、PerpendicularBisector[A, B](中垂线)、AngleBisector[A, B, C](角平分线)
函数
f(x) = x^2 + 2x + 1- 三角函数
sin/cos/tan与反三角asin/acos/atan exp(x)或e^x- 对数三兄弟:
ln(x)、lg(x)(以 10 为底,禁用log(10,x))、ld(x) sqrt/cbrt/abs/floor/ceil/round- 分段函数
If[x<0, -x, x] - 微积分
Derivative[f]、Integral[f, a, b]
圆锥曲线
- 圆:
Circle[M, r]、Circle[M, A]、Circle[A, B, C],方程c: x^2 + y^2 = 9 - 椭圆:
Ellipse[F1, F2, a],方程须用整数系数(如9x^2 + 16y^2 = 144),避免分数 - 双曲线
Hyperbola[F1, F2, a]、抛物线Parabola[F, line]
多边形 / 角
Polygon[A, B, C](多边形)、Polygon[A, B, n](正 n 边形)、Angle[A, B, C]
变换
Translate / Rotate / Reflect / Dilate
样式(对象必须先创建、后设样式)
- 颜色:
SetColor[obj, "Blue"]或SetColor[obj, r, g, b] SetLineThickness[obj, 1-13](线宽区间)SetLineStyle[obj, 0/1/2](0 实线 / 1 虚线 / 2 点线)SetPointSize[obj, 1-9](点大小区间)SetVisible[obj, false](隐藏辅助对象)、SetLabelVisible、SetCaption
画布
ShowGrid[true/false]、ShowAxes[true/false]- 明确禁用
SetCoordSystem——坐标系交给 GeoGebra 自动适配
文字
Text["内容", (2,3)];LaTeX 写法Text["$\\frac{1}{2}$", (0,0)]
六、高频错误清单:给模型划出的红线
提示词特意列举了视觉模型生成 GGB 脚本时最常见的六类语法错误,一一给出反例与正例:
- 圆括号当参数分隔符:❌
Circle(A, 3)/Line(A, B)→ ✅ 一律方括号Circle[A, 3]、Line[A, B]; - 错误构造点:❌
Point({1,2})→ ✅A = (1, 2); - 错误的对数语法:❌
log(10, x)→ ✅lg(x); - 方程含分数系数:❌
x^2/4 + y^2/9 = 1→ ✅ 整数系数9x^2 + 4y^2 = 36; - 用
#写注释:GeoGebra 原生脚本不支持注释语法(后文会看到,DeepTutor 渲染层实际上自己用#做描述行解析,这是两者的重要区别); - 把"图中自由点"写成派生点:再次重申反假设原则。
七、生成顺序:对象依赖驱动的构建流水线
为了让每条命令执行时其依赖对象已存在,提示词规定了固定的生成顺序:
画布设置 → 基准点(题干坐标)→ 派生点(命令)→ 自由点(估算坐标)→ 线段/图形 → 辅助构造(辅助线用完
SetVisible[..., false]隐藏)→ 样式。先建对象再设样式。确保所有图中可见元素都被创建。
这一顺序与 GeoGebra 的脚本执行模型完全一致:命令逐条顺序执行,被引用的对象必须先于引用者创建。例如PerpendicularBisector[A, B]必须出现在A、B定义之后,SetColor必须出现在对象创建之后。同时,把"辅助构造"单独安排在主体图形之后,并用SetVisible[..., false]隐藏——既保证几何关系的可计算性,又不污染最终呈现的画面。渲染层对该顺序的依赖可见于 web/components/Geogebra.tsx:前端在appletOnLoad回调中逐条调用api.evalCommand(cmd),单条命令失败不会中断其余命令。
八、输出协议:严格的结构化 JSON
提示词要求模型"只输出一个 JSON"(可包在 ```json 代码块里),结构如下,不得输出任何多余文字:
{ "image_is_reference": true, "image_reference_keywords": ["如图"], "constraints": [ {"description": "A的坐标为(-3,0)", "type": "coordinate", "source": "题干"} ], "geometric_relations": [ {"type": "perpendicular", "objects": ["AC", "BD"], "description": "AC 垂直 BD"} ], "commands": [ {"command": "ShowAxes[true]", "description": "显示坐标轴"}, {"command": "A = (-3, 0)", "description": "题干坐标点 A"}, {"command": "B = (2, 0)", "description": "题干坐标点 B"}, {"command": "C = (-0.5, -3)", "description": "图中自由点 C,按图估算坐标"}, {"command": "Segment[A, B]", "description": "连接 AB"} ] }契约要点(原文明确):
commands必须非空,且每条都是合法的 GeoGebra 命令;constraints/geometric_relations可为空数组([])。
这一 JSON 是整条管线的"信息中继站",各字段含义如下:
image_is_reference+image_reference_keywords:记录第三步的权威性判断及命中的引用词;constraints:题干或图中给出的硬性约束(坐标、长度、角度等),每条含description/type/source;geometric_relations:点线之间的几何关系(垂直、平行、相切等),含type/objects/description;commands:有序可执行的 GeoGebra 命令列表,每条含command与给人看的description。
8.1 模型输出如何被容错解析
LLM 直接输出的 JSON 并不总是干净。在 vision_solver_agent.py 的_extract_json中实现了三层容错:
- 用正则优先抓取 ```json 围栏内的内容,无围栏则取整段响应;
- 依次剔除
//行注释与/* */块注释(模型偶尔会在 JSON 里写注释); - 最后兜底:剥离尾随逗号(
,\s*([}\]])→\1),这是大模型最常犯的 JSON 语法错误。
解析失败(json.JSONDecodeError)时会记录告警日志并返回空 dict,交由调用方的"门控修复"处理。
8.2 命令列表的归一化
_coerce_commands(vision_solver_agent.py)把模型的commands归一化为[{command, description}]的标准形态:既接受规范的 dict 列表,也优雅降级接受裸字符串命令,并丢弃所有空项。凡command字段为空或非字符串者一律过滤,从而保证进入渲染层的每条命令都可执行。
九、一次完整调用背后的实现:单次分析与门控修复
梳理 vision_solver_agent.py 的process主流程,能清晰看到整套质量控制逻辑:
async def process(self, question_text, image_base64=None, session_id="default"): if not image_base64: return {"has_image": False, "final_ggb_commands": []} # 无图短路 analysis = await self._analyze(question_text, image_base64) # 单次视觉调用 commands = _coerce_commands(analysis.get("commands")) if not commands: # 门控修复 analysis = await self._analyze(question_text, image_base64, repair=True) commands = _coerce_commands(analysis.get("commands")) return { "has_image": True, "final_ggb_commands": commands, "analysis_output": analysis, "image_is_reference": bool(analysis.get("image_is_reference")), }关键设计点在门控修复(gated repair):只有当首轮输出的commands为空(JSON 解析失败或空列表)时才追加一次修复调用。修复时_analyze会在原提示词后追加一段中文补充指令:"上一次输出未能生成有效的commands。请重新审视图片,确保输出合法 JSON,且commands至少包含一条可执行的 GeoGebra 命令。"首轮即成功的情况下,完全不需要付出第二次调用的延迟与成本。
9.1 输出如何打包给上层
format_ggb_block(vision_solver_agent.py)把命令序列打包成前端可识别的围栏块:
def format_ggb_block(self, commands, page_id="main", title="题目图形") -> str: content = self._format_commands(commands) # 每行一条 command,description 被剥离 if not content: return "" return f"```ggbscript[{page_id};{title}]\n{content}\n```"注意_format_commands只提取每条命令的command字段逐行拼装,而人类可读的description并不进入脚本——真正要执行的是纯命令文本。
9.2 内置工具 geogebra_analysis
这个 Agent 被注册为内置工具geogebra_analysis(deeptutor/tools/builtin/init.py#L568-L594),工具描述为 "Analyze a math problem image, detect geometric elements, and generate validated GeoGebra commands for visualization. Requires an attached image.",接受两个参数:question(题干文本)与image_base64(Base64 图片,data URI 或裸编码,通过 function-calling 时由附件自动注入)。执行时有三个值得注意的防御点:
- 必选图片:无图直接返回
success=False与提示 "This tool requires an image attachment."; - data URI 归一化:
VisionSolverAgent期望完整data:image/<fmt>;base64,…形态,若调用方传入的是裸 Base64,工具会自动补前缀data:image/png;base64,(deeptutor/tools/builtin/init.py#L615-L616)——避免模型幻觉出不存在的 kwarg 导致静默穿透;这里的#注释格式与前端parseGgbCommands中!line.startsWith("#")的过滤逻辑遥相呼应; - 语言注入受控:
language由 chat 管线从用户会话设置注入,绝不接受 LLM 提供的覆盖值(防止模型越权篡改语言),且该 Agent 仅使用get_llm_config()提供的api_key/base_url(无独立视觉模型时回落会话模型)。
工具在调用agent.process成功后,会把 constraints、geometric_relations 的摘要连同 ggbscript 围栏块一并写入返回文本,并把image_is_reference、commands_count、constraints_count、relations_count等放入metadata,供上层 Agent 与前端消费(deeptutor/tools/builtin/init.py#L637-L674)。
十、从后端脚本到前端画板:渲染与校验链
还原出的命令最终要变成用户眼前可交互的 GeoGebra 画板,这一链路涉及三处代码:
10.1 GGBScript 块解析
deeptutor/tools/vision/block_parser.py 定义了```ggbscript[page-id;optional-title]与```geogebra[page-id;optional-title]两种块语法(正则BLOCK_START_PATTERN见其第 39-42 行),并给出:
parse_ggb_blocks:一次性把整段 LLM 文本切成普通文本段 + GGB 块;StreamingBlockParser:支持流式输出过程中增量识别完整块(供逐 token 渲染场景使用);- 每个块在解析时都会调用
validate_ggbscript(content)做语法校验与自动修复,修复差异记录到original_content,警告写入validation_warnings。
10.2 前端 applet 渲染
web/components/Geogebra.tsx 是 web 端的渲染组件:
- 通过共享的单例 loader 加载 GeoGebra 官方部署脚本
deployggb.js(GGB_SCRIPT_SRC),并缓存 Promise,页面中多个 applet 只加载一次; parseGgbCommands按行拆解命令,剔除空行与以#开头的描述行——所以服务端 ggbscript 块中如包含#注释会被前端安全剥离(这也解释了提示词为何要求 GeoGebra 原生命令不含#:原生命令与前端注释两条通道被区分对待);- 每个 applet 以
appName: "geometry"初始化并注入唯一 id 的容器,在appletOnLoad中逐条evalCommand,单条失败仅console.warn不中断整体;隐藏工具栏、代数输入框、菜单栏,仅保留重置按钮与拖拽缩放,为用户呈现"干净的几何画板 + 完整交互能力"。
10.3 校验器与坐标系变换
同目录下还提供 deeptutor/tools/vision/ggb_validator.py(提供validate_ggbscript做命令级校验/修复)与 deeptutor/tools/vision/coord_transform.py(推测用于图像坐标系与 GeoGebra 坐标系之间的换算——与提示词中"图像 y 轴向下、GeoGebra y 轴向上"的提醒对应)。虽然当前 Agent 采用"按图估算坐标 + 锚点换算"的策略,这些配套工具说明项目在 GGB 脚本质量保障上有完整的工程沉淀。
十一、使用场景与测试验证:chat 与 solve 如何调用它
根据源码,该能力在两条主链路被启用:
- solve(解题)能力:在 deeptutor/capabilities/solve/capability.py 的工具名单中,
geogebra_analysis与rag、code_execution、reason并列。solve 的中英文系统提示词(zh/system.md、en/system.md)都指示模型:"带配图的题、或画个图有助于理解的几何题,调用geogebra_analysis把图形还原成 GeoGebra 图形,再据此求解。" - chat 通用对话:
geogebra_analysis是默认启用的内置工具(deeptutor/tools/builtin/init.py 的工具注册表中可见),在 chat 的 agentic pipeline(deeptutor/agents/chat/agentic_pipeline.py)中,当调用geogebra_analysis且函数参数含image的properties时,会从消息附件自动注入图片——测试 tests/agents/chat/test_agent_loop.py 的test_augment_tool_kwargs_injects_geogebra_image专门验证了这一注入逻辑。
仓库测试同样覆盖了工具的成功路径与能力集成:
- tests/core/test_builtin_tools.py 的
test_geogebra_analysis_tool_handles_success用FakeVisionSolverAgent替换真实 Agent,验证execute返回结构与 ggbscript 块拼装逻辑; - tests/core/test_capabilities_runtime.py 的
test_chat_capability_streams_content_and_geogebra_context验证在启用["rag", "web_search", "geogebra_analysis"]的回合中,GeoGebra 上下文能正确流式输出。
十二、配置与扩展:温度、max_tokens 与语言
Vision Solver 作为一项"插件能力"出现在配置体系中。默认配置(deeptutor/services/config/capabilities_settings.py、deeptutor/services/config/loader.py 与初始化脚本 deeptutor/services/setup/init.py 保持一致):
"vision_solver": { "temperature": 0.3, "max_tokens": 12000 }temperature: 0.3:命令生成是"结构化输出"任务,低温度显著降低语法幻觉;max_tokens: 12000:为复杂的几何还原预留充足输出空间(约束列表、关系列表加命令序列可能很长);language:由会话设置注入提示语言(当前提示词正文为中文,面向中文题干场景)。
从模块化设计看,若要适配其他语言题干或升级提示策略,只需调整 prompts/geogebra.md 本身,Agent 的__init__会在启动时按路径动态读取该文件(文件缺失时记录告警并回退为空提示,见 vision_solver_agent.py),因此提示词与代码解耦、可独立迭代。
十三、设计要点总结:如何写好"图 → 交互图形"的 Agent 提示词
从 geogebra.md 这份提示词可以提炼出几条可复用的方法论:
- 显式划分信息来源的权威性(
image_is_reference判断):先让模型决定"信图还是信题",再决定后续推断的置信度,避免文字与图片矛盾时模型自行摇摆; - 用"反假设原则"约束关系推断:把点分成"题干坐标点 / 文字定义的派生点 / 看图估坐标的自由点"三类,并对最隐蔽的过度推断(看图猜中点、交点)写绝对禁令;
- 内嵌工具语法手册 + 高频错误对照表:视觉模型普遍不熟 GGB 语法(方括号、
lg、整数系数方程),给出正反例比泛泛说"注意语法"有效得多; - 强制对象依赖顺序:画布 → 基准点 → 派生点 → 自由点 → 图形 → 辅助构造 → 样式,与脚本解释器逐条执行的模型完全对齐;
- 结构化 JSON 契约 + 运行时容错:
commands非空校验、JSON 解析降级(去注释、去尾逗号)、空命令门控重试,共同兜住大模型的输出不确定性。
这一整套设计使得"题图还原"从一次不可控的视觉生成,收敛为一条低延迟、可校验、可交互的工程管线——也正是 DeepTutor 能在 chat 闲聊与 solve 解题两条主链路中稳定提供可拖拽几何画板的底层原因。想深入了解的读者,建议顺着 vision_solver_agent.py、builtin/init.py 中的 GeoGebraAnalysisTool、block_parser.py 与 web/components/Geogebra.tsx 四份代码按"提示词 → Agent → 工具 → 渲染"的顺序阅读,即可拼出完整调用图景。
【免费下载链接】DeepTutorDeepTutor: Lifelong Personalized Tutoring. https://deeptutor.info/.项目地址: https://gitcode.com/GitHub_Trending/dee/DeepTutor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考