找回密码
 立即注册
首页 业界区 业界 解密prompt系列60. Agent实战:从0搭建Jupter数据分析智 ...

解密prompt系列60. Agent实战:从0搭建Jupter数据分析智能体

雨角 6 小时前
本文将带你从零搭建一个数据分析智能体,实现用户上传Excel并给出指令后,智能体能够深入分析数据、进行可视化,并以Jupyter Notebook形式返回结果。我们将重点讨论以下核心要点:

  • 智能体设计模式:为何全自动React模式可行但并非最优解
  • Context Engineering:如何通过上下文管理思路优化效果
  • 复杂任务Prompt设计:Meta Prompt + 领域最佳实践的高效组合
完整Agent代码详见DAAgent
智能体设计

数据分析智能体本质上是具备编码能力、能够使用编程工具的智能体。考虑到单次编码可能不完善,需要多轮迭代调试优化,最简单的实现方式是采用React策略,让模型不断编写和优化代码。
但线性React模式很快会遇到两个关键问题:

  • 模型上文很快耗尽:coding是很费token的,尤其traceback更是长的离谱,所以很快就报token limit了
  • 线性模式存在推理惯性:同样因为上文太长,模型在修复问题时会让分析也限于局部优化,整个数据分析会简单,广度和深度都不足
为解决以上线性REACT的问题,我们加入Plan模块,先对问题进行拆解,再让REACT去解决每个局部问题,只要子问题足够聚焦,以上两个问题就都能规避。
于是我们第一版Data Agent的设计思路就有了,包含以下3个模块

  • Planner:将数据分析任务拆解为多个串联步骤
  • Publisher:遍历Plan生成的步骤,分发给Coder执行
  • coder:基于当前步骤生成代码,通过多轮循环优化直至执行成功
1.png

下面我们依次说下这三个模块,和中间涉及到的context engineering,meta prompting,MCP使用的一些问题和细节。
Planner

Planner采用结构化推理,输出以下Plan结构体。我们使用一次性规划模式,输入仅包含文件预览和用户查询,不基于后续编码反馈调整规划(简单架构能避免许多复杂问题)。
  1. class Plan(BaseModel):
  2.     reasoning: str = Field(description='step by step的分析')
  3.     task: str = Field(description='分析任务名称')
  4.     steps: List[str] = Field(description='拆解的分析步骤')
  5. class Status(str, Enum):
  6.     waiting = 'WAITING'
  7.     finish = 'FINISH'
  8.     fail = 'FAIL'
  9.     in_progress = 'IN-PROGRESS'
  10. class StepStatus(TypedDict):
  11.     description: str
  12.     observation: str
  13.     status: Status
  14. async def plan(state: CodeState, config: RunnableConfig, writer: StreamWriter):
  15.     """
  16.     创建数据分析计划
  17.     """
  18.     configurable = Configuration.from_runnable_config(config)
  19.     llm = ChatDeepSeek(
  20.         api_key=os.environ['LLM_APIKEY'],
  21.         api_base=os.environ['LLM_URL'],
  22.         model=configurable.plan_model
  23.     )
  24.     prompt = get_prompt_template(name="plan", input_vars={"table": state['data'].head(10).to_markdown()})
  25.     messages = [{"role": "system", "content": prompt},
  26.                 {"role": "user", "content": state["query"]}]
  27.     output = llm.with_structured_output(Plan, method='json_mode').invoke(messages)
  28.     writer(f'创建数据分析计划:{output.task}\n' + '\n'.join(output.steps))
  29.     steps = [StepStatus(description=i, observation='', status=Status.waiting) for i in output.steps]
  30.     return {"task": output.task, "steps": steps}
复制代码
Plan的System Prompt,我使用了Meta-prompting配合模型梳理的数据分析(EDA)的最佳实践指南来生成。前面在Agent Context Engineering - 多智能体代码剖析我们有提过meta-prompting,今天再增加一个就是领域最佳实践概念。
标准化任务如编码、写作都可以用MetaPrompt自动补充任务细节,但这样生成的Prompt往往普通,因为它只是随机采样的一条可行路径而非最优路径。
这时就有两种简单的优化思路:

  • 前置引入专家经验:基于该领域专家经验抽象成最佳实践
  • 后置不断总结经验:基于测试集运行的结果进行常见问题的抽象并优化prompt(并不推荐人工总结,因为human prior!=machine prior)
这里我们采用方案1,直接用DeepSeek联网总结Kaagle GrandMaster在数据EDA上的专家经验和流程再输入Meta Prompt就得到了以下system Prompt
  1. 作为Google的顶尖数据分析和可视化专家,请严格按以下数据分析步骤和分析要点处理用户提交的Excel表格和分析要求,并按照输出格式直接输出JSON
  2. ## Excel表格
  3. {{table}}
  4. ## 当前时间
  5. {{current_date}}
  6. ## 数据分析步骤
  7. 1. 数据初窥与质量评估
  8.    **目标**:了解数据原始面貌和基本健康状况
  9.    - 查看数据形状、类型和基本统计信息
  10.    - 识别缺失值和重复值
  11.    - 理解字段业务含义
  12. 2. 数据清洗与预处理
  13.    **目标**:解决数据质量问题,准备干净数据
  14.    - 按策略处理缺失值(删除/填充/标记)
  15.    - 识别并合理处理异常值(不盲目删除)
  16.    - 统一数据格式和类别命名
  17. 3. 单变量分析
  18.    **目标**:深入理解每个变量的分布特征
  19.    - 数值变量:分析分布、偏度和峰度
  20.    - 分类变量:检查频次和类别平衡
  21.    - 记录需要进行的转换(如对数变换)
  22. 4. 多变量分析与可视化
  23.    **目标**:探索变量间关系,发现数据模式
  24.    - 数值vs数值:散点图+相关矩阵
  25.    - 分类vs数值:小提琴图/箱线图
  26.    - 分类vs分类:堆叠条形图+卡方检验
  27.    - 时间序列:折线图+滚动统计量
  28. 5. 简单建模辅助分析
  29.    **目标**:用量化方法验证分析发现
  30.    - 线性回归:关注系数而非R²
  31.    - 决策树/随机森林:分析特征重要性
  32.    - 所有分析以理解关系为目的,而非预测精度
  33. 6. 报告生成
  34.    **输出**:包含数据质量报告、关键发现、可视化图表和建议的数据分析报告
  35. ## 数据分析要点
  36. 1. **需求解析**
  37.     - 明确用户核心分析目标及潜在需求
  38.     - 判断需加强分析的隐藏维度(如数据对比/聚合维度)
  39. 2. **方案设计**
  40.     - 基于目标定制分析路径(描述性/预测性/诊断性分析)
  41.     - 按优先级匹配可视化方案(如动态仪表盘>静态图表)
  42. 3. **反向工程验证**
  43.     - 论证每个分析步骤如何解决核心需求
  44.     - 确认可视化类型与数据特性的适配性
  45. ## 关键要求
  46. - ❗ 推理(reasoning)必须包含:数据特征发现 → 需求拆解 → 方案推导的完整逻辑链
  47. - ✖️ 禁止在reasoning出现结论性语句(结论仅出现在steps中)
  48. - 使用方括号标注示例中的动态参数(如 [指标名])
  49. - JSON键名严格保持原文格式,不添加注释或额外字段
  50. - steps需包含具体技术实现(如:"使用PCA降维后生成3D散点图")
  51. - 若原始需求模糊,需在reasoning中反推合理分析路径
  52. - 无需输出```json```,直接输出JSON
  53. ## 输出格式
  54. {
  55. "reasoning": "分步数据观察和需求解析过程(先分析后结论)",
  56. "task": "≤15字分析任务标题",
  57. "steps": [
  58.     "步骤1: [如: 观测表格数据并检查数据质量]",
  59.     "步骤2: [如: 进行数据清洗和必要字段归一化处理]",
  60. ]
  61. }
复制代码
以下就是结构化推理得到的数据分析任务和8个串联的执行步骤
2.png

个人还是比较倾向于直接使用官方API,而非langchian包装的各种structure output,bind tools啥的llm推理接口,因为langchain增加了报错排查的难度以及问题隐藏的深度。有个梗就是开发越快的工具debug越慢~
Publisher

生成分析任务后就会进入任务分发节点,这个节点不做模型推理,只做任务状态管理和分发,属于中转节点。coding每完成一个step任务,会重新回到publisher,修改任务状态,再进入新的一轮任务执行,如下
  1. async def publisher(state: CodeState, writer: StreamWriter) -> Command[Literal["__end__", "coding"]]:
  2.     """
  3.     分发数据分析任务,判断任务完成状态
  4.     """
  5.     steps = state['steps']
  6.     # 每次进入都尝试获取最新的ipynb
  7.     tools = await mcp_client.get_tools()
  8.     stop_tool = [i for i in tools if i.name == 'close_sandbox'][0]
  9.     # 所有任务都执行则判断完成
  10.     if all([i['status'] != Status.waiting for i in steps]):
  11.         writer('所有步骤执行完毕!')
  12.         return Command(goto='__end__')
  13.     # 找到下一个待执行的任务,清空历史code message和observations并开始执行
  14.     for step in steps:
  15.         if step['status'] == Status.waiting:
  16.             step['status'] = Status.in_progress
  17.             writer(f'执行步骤-{step["description"]}')
  18.             break
  19.     return Command(goto='coding', update={"steps": steps,
  20.                                           # 清空step内传递信息
  21.                                           "code_messages": ["CLEAR"],
  22.                                           "step_observations": ["CLEAR"],
  23.                                           "step_observations_clean": ["CLEAR"],
  24.                                           "code_round": 0,
  25.                                           # 多步step信息累计
  26.                                           "observations": state["step_observations_clean"]})
复制代码
这里用到Context Engineer-过滤大法,从任务分发上可以看出,也就是多个Step的coding任务之间,不传递code message,只传递过滤Error的bservation,可以大大减少上文Token量级。那要如何设计coding任务才能之基于观测进行串行任务编程呢?咱接着往后聊~
Coding

Coding是最核心的任务。虽然Plan的任务分解通过降低子任务复杂度来减少Step内部的上下文长度,但我们仍需处理多编码步骤间的信息传递问题。这里选择了内外层消息隔离的模式

  • 内层(单个Step内):消息线性增长,每一步向后传递code + execution result。同时通过指令让模型print所有代码执行过程中的必要观测,并对所有数据分析的中间结果进行持久化存储
  • 外层(多个step之间):因为观测和中间结果都在Studout中直观显示,所以多步之间只需传递过滤Error的Execution Result,后面章节我们再考虑引入更多压缩和反思极值进一步提升信息密度,来应对更复杂的问题。
以下是关于持久化和stdout输出的相关指令,完整指令详见DAAgent
[code]3. **数据流规范**:禁止使用全局变量传递数据,使用持久化文件进行信息传递,例如   | 步骤类型 | 输入文件          | 输出文件格式要求         |   |----------|-------------------|------------------------|   | 数据加载 | raw_data.csv      | /                      |   | 中间处理 | [上一步].csv      | 带时间戳的CSV/JSON文件 |   | 可视化   | processed_data/* | 600dpi PNG(带图例)   |4. **分析结果和观测显性化**:print所有结果   | 步骤类型 | print       |    |----------|-------------------|   | 文件输出 |
来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除

相关推荐

您需要登录后才可以回帖 登录 | 立即注册