基于LangGraph与GPT-4构建AutoFigure智能体:从文本描述自动生成科学图表
在实际科研、数据分析和技术文档撰写中我们经常需要将复杂的文本描述如实验数据、统计结果、算法流程转化为清晰、专业的科学图表。传统流程依赖人工在绘图软件如 Python 的 Matplotlib、R 的 ggplot2中编写代码不仅耗时而且对非编程背景的研究者门槛较高。随着 AI 智能体技术的发展一种新的范式正在兴起通过自然语言描述由智能体自动解析意图、处理数据并生成图表实现文档智能的自动化流水线。AutoFigure 正是这一理念下的一个概念性工具或框架。它并非某个具体的开源库而是一种构建思路其核心在于将大语言模型的自然语言理解能力与专业的图表生成引擎如 Plotly、Matplotlib相结合通过编排好的“流水线”或“工作流”将“文本描述”到“科学图表”的生成过程自动化、智能化。这类似于在 Dify、Coze 等平台上搭建一个具备特定技能的智能体或者使用 LangGraph 等框架编排一个多步骤的 AI 工作流。本文将以一个工程实践者的视角带你从零构建一个具备“AutoFigure”能力的智能体流水线原型。我们将使用 Python 生态中成熟的工具模拟实现从接收用户自然语言请求到理解意图、提取或模拟数据再到生成并返回图表文件的完整流程。通过这个过程你将理解智能体文档智能流水线的核心组件、设计模式以及实际开发中需要关注的细节和陷阱。1. 理解智能体文档智能流水线的核心架构一个完整的“文本到图表”智能体流水线远不止是调用一个 AI 绘图接口。它需要一套严谨的架构来处理意图的模糊性、数据的真实性以及输出的专业性。1.1 流水线的核心阶段与挑战典型的 AutoFigure 流水线可以分为四个核心阶段每个阶段都面临特定的挑战意图解析与任务规划智能体需要理解用户描述中的核心要素。例如“请画一个展示过去五年公司营收增长趋势的折线图”这句话中需要提取出图表类型折线图、数据主体公司营收、维度时间-过去五年、度量增长趋势。挑战在于自然语言的歧义性和信息缺失。数据获取与处理根据解析出的意图智能体需要找到或生成对应的数据。这可能涉及查询数据库、调用 API 获取实时数据、从用户上传的文件中解析或者在缺乏真实数据时根据描述逻辑生成模拟数据。这是流水线中最容易出错的环节。图表生成与配置将处理好的数据按照图表类型和最佳实践配置颜色、标签、标题、图例等视觉元素并调用底层图表库生成图像文件如 PNG、SVG或交互式图表对象如 Plotly JSON。结果交付与迭代将生成的图表以合适的方式如图片文件、Base64 编码、网页嵌入代码返回给用户。高级的流水线还应支持基于用户反馈如“把颜色改成红色”、“把 Y 轴改为对数刻度”进行图表的迭代修改。1.2 关键技术组件选型为了构建这个流水线我们需要选择合适的工具。以下是一个基于当前主流技术的选型建议组件推荐技术作用与说明智能体/大模型OpenAI GPT-4/3.5, Claude, 本地部署的 Llama 3.1/2 等负责核心的意图解析、任务规划有时也参与数据模拟和代码生成。编排框架LangChain, LangGraph, AutoGen用于将大模型、工具函数、记忆等组件连接成一个可控的工作流。LangGraph 特别适合有循环、条件分支的复杂流程。图表生成引擎Plotly, Matplotlib, Seaborn, Altair负责最终的图表渲染。Plotly 生成交互式图表优势明显Matplotlib 是静态图表的基石可控性强。数据工具Pandas, NumPy用于数据处理、转换和模拟数据生成。开发/部署平台Dify, Coze, 自行搭建 FastAPI 服务Dify/Coze 提供低代码的智能体搭建界面适合快速原型自行搭建服务则灵活性最高。在本实践中我们将选择LangChainLangGraph OpenAI GPT-4 Plotly的组合自行搭建一个轻量级的 FastAPI 服务以体现最大的灵活性和学习价值。2. 环境准备与项目初始化在开始编码前需要确保你的开发环境已就绪。我们将创建一个独立的 Python 项目。2.1 环境与依赖配置首先确保你已安装 Python 3.9。然后使用pip安装核心依赖库。# 创建项目目录并进入 mkdir autofigure-agent-pipeline cd autofigure-agent-pipeline # 创建虚拟环境推荐 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate # 安装核心依赖 pip install langchain langchain-openai langgraph plotly pandas kaleido fastapi uvicorn python-dotenv关键依赖说明langchain,langgraph: 智能体工作流编排的核心框架。langchain-openai: LangChain 对 OpenAI 模型的官方集成。plotly: 用于生成交互式和静态图表。kaleido: Plotly 的静态图像导出引擎用于生成 PNG。pandas: 数据处理和模拟数据生成。fastapi,uvicorn: 用于构建提供服务的 Web API。python-dotenv: 管理环境变量如 API 密钥。2.2 项目结构与关键文件创建以下项目结构这有助于代码的组织和维护。autofigure-agent-pipeline/ ├── .env # 存储敏感信息如 OPENAI_API_KEY ├── main.py # FastAPI 应用主入口 ├── pipeline/ # 智能体流水线核心模块 │ ├── __init__.py │ ├── agent_workflow.py # 定义 LangGraph 工作流 │ ├── chart_generator.py # 图表生成工具函数 │ └── data_simulator.py # 数据模拟与处理工具函数 ├── utils/ # 工具函数 │ ├── __init__.py │ └── file_utils.py # 文件保存、Base64编码等 └── requirements.txt # 项目依赖列表创建requirements.txt文件内容与上述pip install命令一致。创建.env文件并填入你的 OpenAI API 密钥或其他模型供应商的密钥。# .env 文件内容示例 OPENAI_API_KEYsk-your-openai-api-key-here3. 构建智能体工作流LangGraph这是流水线的大脑我们将使用 LangGraph 来定义从接收到用户请求到最终输出的完整状态流转。3.1 定义工作流状态首先在pipeline/agent_workflow.py中我们定义一个State类用于在工作流的各个节点间传递信息。# pipeline/agent_workflow.py from typing import TypedDict, Optional, List, Dict, Any import pandas as pd import plotly.graph_objects as go class AgentState(TypedDict): 定义智能体工作流的状态 # 输入 user_input: str # 用户的原始文本描述 # 中间产物 parsed_intent: Optional[Dict[str, Any]] # 解析出的意图如 {“chart_type”: “line”, “entities”: [“revenue”, “5 years”]} data_frame: Optional[pd.DataFrame] # 处理后的数据Pandas DataFrame chart_spec: Optional[Dict[str, Any]] # 图表规格如 {“type”: “line”, “x”: “year”, “y”: “revenue”} plotly_figure: Optional[go.Figure] # 生成的 Plotly 图形对象 # 输出 output_message: str # 给用户的文本回复 chart_image_path: Optional[str] # 生成的图表图片保存路径 error: Optional[str] # 错误信息3.2 实现工作流节点我们将工作流分解为几个连续的节点Node每个节点是一个函数接收并更新AgentState。# pipeline/agent_workflow.py (续) from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langgraph.graph import StateGraph, END import json from pipeline.data_simulator import simulate_data_based_on_intent from pipeline.chart_generator import generate_plotly_figure # 初始化大模型 llm ChatOpenAI(modelgpt-4-turbo-preview, temperature0) # 使用 gpt-4 以获得更好的推理能力 def parse_user_intent(state: AgentState) - AgentState: 节点1解析用户意图 prompt ChatPromptTemplate.from_messages([ (system, 你是一个专业的图表分析助手。请从用户的描述中提取生成图表所需的关键信息。 请以 JSON 格式返回包含以下字段 - chart_type: 图表类型如 line, bar, scatter, pie。 - data_subject: 数据主题如 company revenue, temperature。 - x_axis: X轴可能是什么如 year, category。 - y_axis: Y轴可能是什么如 value, count。 - time_range: 时间范围如 last 5 years。 - other_requirements: 其他要求如 use log scale。 如果某个字段无法确定请设为 null。), (human, {user_input}) ]) chain prompt | llm response chain.invoke({user_input: state[user_input]}) try: # 尝试解析模型返回的 JSON parsed json.loads(response.content) state[parsed_intent] parsed state[output_message] f已解析您的意图需要绘制一个关于{parsed.get(data_subject)}的{parsed.get(chart_type)}图。 except json.JSONDecodeError: state[error] f无法解析模型返回的意图{response.content} return state def fetch_or_simulate_data(state: AgentState) - AgentState: 节点2获取或模拟数据 if state.get(error): return state intent state[parsed_intent] # 在实际项目中这里可以连接数据库或API。此处我们模拟数据。 try: df simulate_data_based_on_intent(intent) state[data_frame] df state[output_message] f\n已根据意图模拟生成了 {len(df)} 条数据。 except Exception as e: state[error] f数据模拟失败{str(e)} return state def plan_chart_specification(state: AgentState) - AgentState: 节点3规划图表规格 if state.get(error) or state[data_frame] is None: return state intent state[parsed_intent] df state[data_frame] # 这里可以根据意图和 DataFrame 的列名智能地映射 x, y 轴。 # 这是一个简化版假设 DataFrame 有两列第一列为X第二列为Y。 columns df.columns.tolist() if len(columns) 2: chart_spec { type: intent.get(chart_type, line), x: columns[0], y: columns[1], title: f{intent.get(data_subject, Data)} Chart, xaxis_title: intent.get(x_axis, columns[0]), yaxis_title: intent.get(y_axis, columns[1]), } state[chart_spec] chart_spec else: state[error] 生成图表规格失败数据列不足。 return state def generate_chart(state: AgentState) - AgentState: 节点4生成图表 if state.get(error) or state[chart_spec] is None: return state try: fig generate_plotly_figure(state[data_frame], state[chart_spec]) state[plotly_figure] fig state[output_message] \n图表已生成成功。 except Exception as e: state[error] f图表生成失败{str(e)} return state3.3 组装工作流图将上述节点连接起来形成一个线性的工作流。# pipeline/agent_workflow.py (续) def create_workflow() - StateGraph: 创建并返回定义好的工作流图 workflow StateGraph(AgentState) # 添加节点 workflow.add_node(parse_intent, parse_user_intent) workflow.add_node(fetch_data, fetch_or_simulate_data) workflow.add_node(plan_chart, plan_chart_specification) workflow.add_node(generate, generate_chart) # 设置边定义执行顺序 workflow.set_entry_point(parse_intent) workflow.add_edge(parse_intent, fetch_data) workflow.add_edge(fetch_data, plan_chart) workflow.add_edge(plan_chart, generate) workflow.add_edge(generate, END) return workflow.compile() # 全局工作流实例 graph create_workflow()4. 实现数据模拟与图表生成工具工作流节点依赖的工具函数需要具体实现。4.1 数据模拟器在pipeline/data_simulator.py中我们根据解析出的意图生成一个合理的 Pandas DataFrame。# pipeline/data_simulator.py import pandas as pd import numpy as np from datetime import datetime, timedelta from typing import Dict, Any def simulate_data_based_on_intent(intent: Dict[str, Any]) - pd.DataFrame: 根据解析的意图模拟数据。 这是一个示例函数实际项目应根据业务逻辑连接真实数据源。 chart_type intent.get(chart_type, line) data_subject intent.get(data_subject, Sample Data).lower() time_range intent.get(time_range) df None # 示例1模拟时间序列数据用于折线图、面积图 if chart_type in [line, area] and (year in data_subject or month in data_subject or revenue in data_subject): dates pd.date_range(enddatetime.today(), periods12, freqM) # 过去12个月 values np.random.randn(12).cumsum() 100 # 随机游走模拟趋势 df pd.DataFrame({month: dates.strftime(%Y-%m), value: values}) # 示例2模拟分类数据用于柱状图、饼图 elif chart_type in [bar, pie]: categories [A, B, C, D, E] values np.random.randint(10, 100, sizelen(categories)) df pd.DataFrame({category: categories, count: values}) # 示例3模拟散点图数据 elif chart_type scatter: x np.random.randn(50) * 10 50 y x * 0.8 np.random.randn(50) * 5 20 df pd.DataFrame({x_value: x, y_value: y}) # 默认情况生成一个简单的二维数据 if df is None: x list(range(1, 11)) y [i * 2 np.random.randn() for i in x] df pd.DataFrame({x: x, y: y}) return df4.2 图表生成器在pipeline/chart_generator.py中我们根据数据和规格使用 Plotly 生成图表。# pipeline/chart_generator.py import plotly.graph_objects as go import plotly.express as px import pandas as pd from typing import Dict, Any def generate_plotly_figure(df: pd.DataFrame, chart_spec: Dict[str, Any]) - go.Figure: 根据数据和图表规格生成 Plotly Figure 对象。 chart_type chart_spec.get(type, line) x_col chart_spec.get(x) y_col chart_spec.get(y) if x_col not in df.columns or y_col not in df.columns: raise ValueError(f数据框中未找到指定的列: x{x_col}, y{y_col}) fig None # 使用 Plotly Express 快速创建基础图表 if chart_type line: fig px.line(df, xx_col, yy_col, titlechart_spec.get(title)) elif chart_type bar: fig px.bar(df, xx_col, yy_col, titlechart_spec.get(title)) elif chart_type scatter: fig px.scatter(df, xx_col, yy_col, titlechart_spec.get(title)) elif chart_type pie: # 饼图通常需要一个数值列和一个分类列 fig px.pie(df, namesx_col, valuesy_col, titlechart_spec.get(title)) else: # 默认使用线图 fig px.line(df, xx_col, yy_col, titlechart_spec.get(title)) # 更新坐标轴标签 fig.update_xaxes(title_textchart_spec.get(xaxis_title, x_col)) fig.update_yaxes(title_textchart_spec.get(yaxis_title, y_col)) # 应用其他要求例如对数刻度这是一个扩展点 if chart_spec.get(other_requirements) and log scale in chart_spec[other_requirements].lower(): fig.update_yaxes(typelog) return fig5. 封装为 API 服务并运行验证最后我们将工作流封装成一个 FastAPI 服务提供简单的 HTTP 接口。5.1 创建 FastAPI 主应用在main.py中创建 API 端点。# main.py from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse, JSONResponse from pydantic import BaseModel from pipeline.agent_workflow import graph from utils.file_utils import save_plotly_figure, generate_unique_filename import os from dotenv import load_dotenv load_dotenv() # 加载 .env 文件中的环境变量 app FastAPI(titleAutoFigure Agent Pipeline API) class ChartRequest(BaseModel): description: str # 用户对图表的文本描述 app.post(/generate_chart/) async def generate_chart(request: ChartRequest): 接收文本描述返回图表生成结果。 # 初始化工作流状态 initial_state { user_input: request.description, parsed_intent: None, data_frame: None, chart_spec: None, plotly_figure: None, output_message: , chart_image_path: None, error: None } try: # 执行工作流 final_state graph.invoke(initial_state) except Exception as e: raise HTTPException(status_code500, detailf工作流执行异常: {str(e)}) # 检查错误 if final_state.get(error): raise HTTPException(status_code400, detailfinal_state[error]) # 保存图表为图片文件 if final_state.get(plotly_figure): filename generate_unique_filename(prefixchart_, suffix.png) filepath save_plotly_figure(final_state[plotly_figure], filename) final_state[chart_image_path] filepath # 返回结果包括消息和图片访问路径 return { message: final_state[output_message], chart_url: f/chart_image/{filename}, # 提供访问图片的URL details: { intent: final_state.get(parsed_intent), data_preview: final_state.get(data_frame).head().to_dict(orientrecords) if final_state.get(data_frame) is not None else None } } else: raise HTTPException(status_code500, detail图表生成失败未得到图形对象。) app.get(/chart_image/{filename}) async def get_chart_image(filename: str): 提供生成的图表图片访问。 filepath os.path.join(generated_charts, filename) if not os.path.exists(filepath): raise HTTPException(status_code404, detail图片未找到) return FileResponse(filepath, media_typeimage/png) if __name__ __main__: import uvicorn uvicorn.run(app, host0.0.0.0, port8000)5.2 实现文件工具函数在utils/file_utils.py中添加保存图片和生成文件名的工具。# utils/file_utils.py import os import uuid from datetime import datetime import plotly.graph_objects as go # 确保存储目录存在 CHARTS_DIR generated_charts os.makedirs(CHARTS_DIR, exist_okTrue) def generate_unique_filename(prefix, suffix.png): 生成一个唯一的文件名 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) unique_id str(uuid.uuid4())[:8] return f{prefix}{timestamp}_{unique_id}{suffix} def save_plotly_figure(fig: go.Figure, filename: str) - str: 将 Plotly 图形保存为 PNG 文件 filepath os.path.join(CHARTS_DIR, filename) # 注意需要安装 kaleido 库 fig.write_image(filepath, enginekaleido) return filepath5.3 运行与测试启动服务在项目根目录下运行。python main.py服务将在http://127.0.0.1:8000启动。发送请求测试使用curl或 Postman 等工具。curl -X POST http://127.0.0.1:8000/generate_chart/ \ -H Content-Type: application/json \ -d {description: 请画一个展示过去五年公司营收增长趋势的折线图}预期会收到一个 JSON 响应包含message、chart_url和details。访问chart_url指向的地址即可看到生成的 PNG 图片。查看结果生成的图片会保存在项目根目录的generated_charts/文件夹下。6. 常见问题排查与优化在实际运行中你可能会遇到以下问题。这里提供排查思路和优化方向。6.1 工作流执行失败排查表问题现象可能原因检查方式处理建议请求返回500错误日志显示OpenAI API相关错误。1. API 密钥未设置或错误。2. 网络问题导致连接超时。3. 账户余额不足。1. 检查.env文件中的OPENAI_API_KEY。2. 在命令行用curl测试 OpenAI 接口连通性。3. 登录 OpenAI 控制台检查额度。1. 确保密钥正确且已导出到环境。2. 配置网络代理或检查防火墙。3. 充值或更换 API 密钥。请求返回400错误提示“无法解析模型返回的意图”。大模型没有返回合法的 JSON 格式。打印response.content查看模型返回的原始文本。1. 调整提示词Prompt明确要求返回纯 JSON。2. 使用 LangChain 的output_parsers如JsonOutputParser来强制解析。图表生成成功但数据明显不符合描述如要柱状图却生成了折线图。1. 意图解析不准确。2. 数据模拟逻辑与意图不匹配。3. 图表规格映射错误。1. 检查返回的parsed_intent。2. 检查data_frame的内容和列名。3. 检查chart_spec的内容。1. 优化意图解析的提示词加入更多示例Few-shot。2. 增强simulate_data_based_on_intent函数的逻辑。3. 在plan_chart_specification节点加入更智能的列名匹配。服务能运行但生成图片时报kaleido相关错误。1.kaleido未正确安装。2. 系统缺少图形依赖常见于无 GUI 的服务器。1. 确认 pip listgrep kaleido。br2. 查看错误日志是否提示libGL 等缺失。工作流执行速度慢。1. 大模型 API 调用延迟高。2. 数据模拟或图表生成逻辑复杂。使用time模块记录各节点耗时。1. 考虑使用更快的模型如gpt-3.5-turbo或本地模型。2. 对模拟数据等操作进行缓存。3. 将工作流异步化FastAPI 支持async。6.2 从原型到生产环境的优化建议上述代码是一个教学原型。要用于实际生产或更复杂的场景需要考虑以下优化增强意图解析能力结构化输出使用 LangChain 的PydanticOutputParser定义严格的输出格式提高解析成功率。多轮对话当前是单次请求。复杂需求可能需要多轮澄清。可以引入Memory组件并将工作流改造成支持循环LangGraph的Conditional Edge。领域特定优化为科研、金融、电商等不同领域定制专门的提示词和实体识别逻辑。接入真实数据源将fetch_or_simulate_data节点改造为“工具调用”节点。智能体可以根据意图决定调用哪个数据查询工具Tool例如查询 MySQL、调用内部 API、读取 CSV 文件等。LangChain 的Tool机制非常适合此场景。提升图表专业性模板化为不同图表类型如学术论文图、商业报表图预定义配色方案、字体、布局模板。异常处理检查数据是否为空、类型是否匹配并给出友好的错误提示。多图表支持扩展流水线支持在一个请求中生成子图Subplots或仪表板。工程化与部署配置管理将模型类型、API 端点、文件存储路径等抽离到配置文件中。日志与监控为工作流的每个节点添加详细日志并集成 Prometheus 等监控追踪耗时、成功率和错误类型。异步处理对于耗时的图表生成任务应改为异步接口先返回任务 ID客户端再轮询结果。安全性对用户输入进行清洗防止 Prompt 注入攻击对生成的文件名进行安全检查防止路径遍历。探索更先进的架构多智能体协作可以拆分为“需求分析智能体”、“数据查询智能体”、“图表设计智能体”通过 LangGraph 的State进行协作和辩论得到更优结果。集成低代码平台将本流水线作为后端引擎为类似 Dify、Coze 这样的平台提供一个“图表生成”技能Skill从而利用其已有的用户界面、知识库和插件生态。构建 AutoFigure 智能体流水线的过程本质上是将人类设计图表的专业知识通过大语言模型的理解能力和程序化的工具调用进行编码和自动化。从简单的文本描述到最终的可视化图表这条流水线上的每一个环节——意图解析、数据桥接、视觉编码——都充满了挑战和优化的空间。本文提供的原型是一个坚实的起点你可以在此基础上根据具体的业务需求和数据环境持续迭代和强化各个环节最终打造出一个真正高效、可靠的文档智能生产力工具。