从 LangGraph 死循环到 Skill 驱动:我把 Text2SQL 升级成了SKILL模式
unsetunset一、为什么我把 Text2SQL 从 LangGraph 升级成 Skill 模式unsetunsetAix-DB 做到 v1.2.2 之后Text2SQL数据问答这条线其实已经跑得很稳了。我一开始用 LangGraph 的StateGraph串了一条七阶段管线——datasource_selector → schema_inspector → sql_generator → permission_filter → sql_executor → chart_generator → summarizer——每个节点职责清晰但它有一个一个没法绕过去的问题业务需求的迭代速度永远比代码的迭代速度快。SKILL模式就能很好的解决这个问题。第一、产品提一个新场景我就要改图。比如产品同学说再加一种’趋势归因’的分析能力用户问’为什么 8 月销售特别高’的时候要同时出月度趋势和品类分解两条 SQL。按老架构我得在sql_generator里加分支、在 state 里多加字段、在chart_generator里多挂一种图。一个简单的业务扩展要改三个地方。第二、硬编码的流程图其实在束缚 LLM 的能力。模型自己已经很聪明了什么时候该先查 schema、什么时候该并发发多条 SQL、什么时候该直接回答它判断得比我写死的流程图更准。我把节点顺序写死等于是在逼一个博士生按小学生的作业格式答题。第三、子能力在不同 Agent 之间没法复用。Aix-DB 里同时跑着Text2SqlAgent、DeepAgent、ExcelAgent三条线schema 探索这件事它们都要做一遍结果每个 Agent 里都抄了一份类似的 Prompt。所以我动手孵化了一个深度问数模块完全走SKILL模式。新架构就在agent/deepagent/deep_research_agent.py这一个文件里核心是create_deep_agent() 四个 Skill技能——schema-exploration、query-writing、report-generation、frontend-design。整条 Text2SQL 的能力不再长在 Python 代码里而是写在 Markdown 里。这篇文章就讲这次重写怎么把七阶段 StateGraph复合成四个 Skill 文件 五把 SQL 小工具 一个 DeepAgent。unsetunset二、整体架构把流程交还给 LLMunsetunset2.1 两种范式的对比先把新旧两套架构并排放一下差距一眼就看得出来维度LangGraph StateGraphDeepAgent Skill流程控制代码硬编码节点顺序LLM 自主决策调用顺序能力扩展改图、改 state、改节点加一个SKILL.md子能力复用每个 Agent 各抄一份 PromptSkill 目录跨 Agent 共享修改生效改代码 → 重启服务改 Markdown → 下一轮对话生效可观测性节点日志散落四阶段PhaseTracker统一单测难度每个节点都要 mock集成测试为主新架构最狠的一点是用户自己就能改 Skill。他不用懂 Python、不用懂 LangGraph、不用懂 StateGraph他只要会写 Markdown就能给 Agent 加一种新的分析能力。技能中心2.2 Skill ≠ Tool想清楚这件事很重要刚接触 deepagents 这个库的时候我被Skill这个词误导过很久——以为 Skill 是一种特殊的工具。不是。Tool 是厨房里的锅铲——sql_db_list_tables、sql_db_schema、sql_db_query是最小行动单元能被 LLM 直接调用Skill 是菜谱——SKILL.md是一份 Markdown 指令文档告诉 LLM “做红烧肉要先焯水再煸糖色”LLM 是厨师——它按菜谱的套路选锅铲做事把这个关系想清楚之后Agent 的设计思路就完全不一样了。你不是在写一个执行器你是在写一本员工手册。2.3 目录结构agent/deepagent/├── AGENTS.md # 主员工手册总纲├── deep_research_agent.py # Agent 壳子 SSE 流处理├── skills/│ ├── schema-exploration/ # 探索 schema 的菜谱│ │ └── SKILL.md│ ├── query-writing/ # 写 SQL 的菜谱│ │ └── SKILL.md│ ├── report-generation/ # 生成 HTML 报告的菜谱│ │ └── SKILL.md│ └── frontend-design/ # 报告前端美学的菜谱│ └── SKILL.md└── tools/ # 锅铲 └── native_sql_tools.py整个 Agent 的能力边界被拆到了这几个 Markdown 文件里。Python 只负责三件事加载 Skill、调模型、推流到前端。2.4 主链路看一眼_create_sql_deep_agent就明白了def _create_sql_deep_agent(self, datasource_id: int, session_id: str): model get_llm(timeoutself.LLM_TIMEOUT, max_tokensself.LLM_MAX_TOKENS) # 根据数据源类型选择 SQL 工具 if db_enum.connect_type ConnectType.sqlalchemy: db SQLDatabase.from_uri(uri, sample_rows_in_table_info3) sql_tools SQLDatabaseToolkit(dbdb, llmmodel).get_tools() else: set_native_datasource_info(...) sql_tools [sql_db_list_tables, sql_db_schema, sql_db_query, sql_db_query_checker, sql_db_table_relationship] # 加载 Skill 目录 AGENTS.md 作为 memory skill_paths [os.path.join(current_dir, skills)] current_date datetime.now().strftime(%Y-%m-%d) memory [os.path.join(current_dir, AGENTS.md), f当前日期: {current_date}] return create_deep_agent( modelmodel, memorymemory, skillsskill_paths, toolssql_tools, backendFilesystemBackend(root_dircurrent_dir), )它没有 StateGraph、没有节点定义、没有 state schema——就是把菜谱塞进 memory、把锅铲塞进 tools剩下的交给 LLM。整个 Text2SQL 的流程从 Python 代码里蒸发了。深度问数unsetunset三、关键实现拆解unsetunset3.1 AGENTS.md给 Agent 一份员工手册而不是流程图AGENTS.md是 Agent 启动时加载的主纲。它不描述先做什么再做什么它描述遇到什么情况该怎么判断。核心片段节选## ⚠️ 执行流程强制遵守### 第一步思考与规划必须先输出在执行任何工具操作之前**必须先输出**你的分析和计划。**需求理解** [用一句话简述用户需求]**执行计划**1. [步骤1 - 如获取相关表结构]2. [步骤2 - 如编写并执行查询]3. [步骤3 - 如分析结果并回答]## ⚠️ 关键行为规则防止循环1. 不要重复调用同一工具2. 不要重复执行相同的 SQL3. 获取表架构后立即使用4. 任务完成后立即停止这里我想强调一点大模型最大的不稳定点是无限循环。它会反复sql_db_list_tables、反复sql_db_schema、反复sql_db_query_checker一个简单问题能烧掉几十次工具调用。我的解法不是在代码里加循环检测虽然ToolCallManager也在兜底而是在员工手册第一页就用中文把规则写清楚。LLM 读得懂它比你想象得听话。3.2 schema-exploration让 LLM 学会先过滤再看表这是我踩过的坑里最深的一个。早期版本我直接让 Agent “先列表再拉 schema”。结果真实业务数据库动辄 200 张表一次sql_db_schema把所有表的 DDL 拉下来prompt 里塞进几万 token模型直接分心。于是schema-exploration/SKILL.md里写了一段智能过滤的指令### 2. 智能表过滤针对复杂查询当数据库表较多时不要盲目获取所有表的 schema而是先进行智能过滤1. **获取表列表后**根据用户问题进行语义分析提取关键实体和意图2. **匹配策略** - 将关键词与表名、表注释进行语义匹配 如销售额 → 可能涉及 orders、sales、products 等表 - 考虑表之间的潜在关联 如用户问客户订单需要同时选中 customers 和 orders - 忽略明显无关的系统表、日志表、临时表3. **输出**筛选后的相关表名列表通常 3-8 张表设计判断Markdown 写得足够结构化LLM 会当它是可执行的伪代码。上面这三步规则我没写一行 Python但模型真的会按列出所有表 → 语义过滤 → 只拉相关表 schema这个顺序执行。配合这段规则Skill 文件里还塞了一份M-Schema 输出模板让 LLM 把 schema 整理成固定格式再传给下一步【DB_ID】 sales_db【Schema】# Table: orders, 订单表[ (id:INTEGER, 订单ID), (customer_id:INTEGER, 客户ID), (amount:DECIMAL, 订单金额),]orders.customer_id customers.idM-Schema 这个格式不是我发明的但把它写进 Skill 里让 LLM 自己组织——这一步把 Text2SQL 的准确率拉高了肉眼可见的一档。3.3 query-writing一次对话生成多条 SQL 的意识单 SQL 的 Text2SQL 已经被玩烂了。真正的业务问题长这样“帮我看下今年的月度销售趋势为什么 8 月特别高”这个问题必须拆成三条 SQLSQL1月度趋势折线图SQL28 月按品类分解柱状图SQL38 月 vs 7 月品类对比瀑布图我把这个多维度查询意识写进了query-writing/SKILL.md## 多维度查询策略| 场景 | 策略 ||------|------|| 趋势归因分析 | SQL1: 时间维度趋势SQL2: 分类维度归因 || 综合报告 | SQL1: 汇总 KPISQL2: 趋势SQL3: 排名 || 为什么类问题 | SQL1: 总体趋势确认变化SQL2: 按维度分解贡献 || 对比分析 | SQL1: 当前周期SQL2: 对比周期 |### 每条查询的元信息为每条 SQL 标注用途和推荐图表类型查询 1获取月度趋势数据推荐图表折线图/面积图SQL: SELECT ...这一段是整个 Skill 模式最让我觉得值回票价的部分。我没写一行代码去识别用户问题是不是归因类、也没加节点去做 SQL 分解。我只是把遇到归因问题要出几条 SQL、每条配什么图用中文说清楚LLM 就能稳定地按这个模式产出。这种感觉像过去你在自己弹奏现在你在指挥。3.4 四阶段 PhaseTracker让思考过程可见Skill 模式跑起来之后有个副作用LLM 在工具调用之前会输出大段思考——分析需求、列计划、推理 schema 结构。这些内容对调试很有用但直接糊到用户脸上就是灾难。用户想看的是北京销售额 120 万不是我分析一下您的需求首先我需要列出所有表…。于是我做了PhaseTracker把 Agent 的输出切成四个阶段class Phase(Enum): PLANNING planning # 思考规划首次工具调用前 EXECUTION execution # 执行回答默认阶段 SUB_AGENT sub_agent # 子代理运行中 REPORTING reporting # HTML 报告生成staticmethoddef _detect_phase(node_name: str, content: str, tracker: PhaseTracker) - Phase: iftaskin node_name.lower(): return Phase.SUB_AGENT ifREPORT_HTML_STARTin content orREPORT_HTML_ENDin content: return Phase.REPORTING ifnot tracker.has_tool_called: return Phase.PLANNING return Phase.EXECUTION判定规则特别朴素首次工具调用之前的所有输出都是思考之后的是执行子代理节点单独标记HTML 报告标记出现就进报告阶段。每个阶段切换时用details标签把对应内容包起来THINKING_SECTION_OPEN ( details stylemargin:8px 0;padding:8px 12px;background:#f8f9fa; border-left:3px solid #4a90d9;border-radius:4px;font-size:14px;color:#555 \n summary stylecursor:pointer;font-weight:600;color:#333 思考与规划/summary\n\n)设计亮点CSS 全部用行内样式。因为公众号环境会剥掉外部样式表只认style...。这是我踩过的坑——早期版本用了 class结果在公众号预览里全都变白底黑字折叠都失效了。![图 3建议截前端展开「 思考与规划」折叠区的效果图左侧蓝色竖条 灰底 思考文字再截一张折叠收起的状态做对比]3.5 astream 多模式流messages updates 双通道这是整个文件最核心的一段——_stream_response方法。deepagents底层是 LangGraphastream支持同时订阅两种流stream_iter agent.astream( input{messages: [HumanMessage(contentquery)]}, configconfig, stream_mode[messages, updates],)messages模式token 级流LLM 每吐一个 token 都推一次用来实现打字机效果updates模式节点级流每个 LangGraph 节点执行完整体推一次用来捕获工具调用和结果两条流并行来主循环里分别处理while True: mode, chunk await asyncio.wait_for( stream_anext(), timeoutself.STREAM_KEEPALIVE_INTERVAL ) if mode messages: # token 级输出走 details 分阶段 message_chunk, metadata chunk token_text self._extract_text(message_chunk.content) new_phase self._detect_phase(node_name, token_text, tracker) if new_phase ! tracker.current_phase: await self._handle_phase_transition(response, tracker, new_phase, node_name) await self._safe_write(response, token_text) elif mode updates: # 工具调用格式化成 SQL 代码块 for node_name, node_output in chunk.items(): for msg in node_output[messages]: await self._process_update_message(msg, response, answer_collector)为什么要双通道如果只走messages模式工具调用的内容只能拿到一堆tool_call_id、function_call的 raw 数据前端没法显示。如果只走updates模式就没了 token 级流式用户要等一整个节点跑完才能看到输出。双通道的妙处是思考部分走 messages逐字流SQL 调用走 updates一次性格式化成代码块前端体验既有实时感又有清晰结构。SQL 工具调用被_format_tool_call格式化成这样staticmethoddef _format_tool_call(name: str, args: dict) - Optional[str]: if name sql_db_query: query args.get(query, ) return f\nsql\n{query.strip()}\n\n elif name sql_db_schema: ...用户在前端看到的就是一个个漂亮的 SQL 代码块跟着一个 ✓ 成功 / ✗ 失败的小标记。深度问数3.6 工程化兜底25s 保活 30 分钟超时 HTML 截断检测公网大模型在高峰期会抽风。你跟 DeepSeek 请求一次它憋 90 秒才吐第一个 token——这时候 Nginx 默认 60 秒无数据就掐连接前端 fetch 也会超时。我在三个层面做了兜底。第一、SSE 保活这个我保证你迟早会遇到STREAM_KEEPALIVE_INTERVAL 25try: mode, chunk await asyncio.wait_for( stream_anext(), timeoutself.STREAM_KEEPALIVE_INTERVAL )except asyncio.TimeoutError: # 25 秒没新数据就发一个 keepalive 心跳 await response.write( data: {data:{messageType: info, content: }, dataType: keepalive}\n\n ) continue25 秒一个心跳Nginx 和浏览器都不会断。第二、任务总超时 30 分钟TASK_TIMEOUT 30 * 60connection_closed await asyncio.wait_for( self._stream_response(agent, config, query, response, ...), timeoutself.TASK_TIMEOUT,)这是最后一道闸。报告生成 多轮 SQL 调用理论上能跑到很长但超过 30 分钟基本是死循环了直接砍掉。第三、HTML 报告截断检测这是我加的偏执小功能full_output .join(answer_collector)if REPORT_HTML_START in full_output and REPORT_HTML_END not in full_output: logger.warning(fHTML 报告被截断 - 会话: {session_id}) truncation_msg ( \n\n ⚠️ **报告生成不完整**: HTML 报告在生成过程中被截断。 可能原因模型输出 token 达到上限。\n !-- REPORT_HTML_END --\n ) await self._safe_write(response, truncation_msg, warning, ...)LLM 生成 HTML 报告时偶尔会因为max_tokens上限提前切断只有!-- REPORT_HTML_START --没有结束标记——前端会一直等结束标记等到天荒地老。这里我主动检测 补一个假的结束标记 告诉用户报告被截断了这比直接挂掉强得多。unsetunset四、Skill 模式落地之后的几条判断unsetunset聊完实现讲几条我自己的提炼。第一、流程图是给人看的不是给 LLM 看的。LangGraph 的 StateGraph 最大的用途是让人理解 Agent 在干嘛。但 LLM 自己并不需要流程图——它看到工具列表和 Skill 文档能自己推理出最优的调用顺序而且比你硬编码的顺序更灵活。第二、Skill 必须是指令文档而不是代码片段。我试过一种反例——把 Skill 写成 Python 函数的模板代码让 LLM 填空。结果模型反而更混乱因为它要在写代码和做决策之间来回切换。改成纯 Markdown 自然语言指令之后准确率立刻上来了。第三、Tool 和 Skill 要彻底解耦。Tool 是最小行动单元不应该包含任何业务逻辑Skill 负责组织这些行动的策略。sql_db_query就是干干净净地执行 SQL至于要不要加 LIMIT、要不要先 checker 一下全在 Skill 文档里说。第四、可观测性比性能重要。用户能看见 Agent 在想什么、在查什么、为什么这么查比快一秒出结果重要得多。details折叠区 token 流式 SQL 代码块这三个东西加起来的信任感远超一个默默跑完的黑盒。unsetunset五、最后unsetunsetAix-DB 现在长这样四个 Text2SQL Skillschema-exploration / query-writing / report-generation / frontend-design五把 SQL 锅铲list_tables / schema / query / query_checker / table_relationship支持 8 种数据库MySQL / PostgreSQL / Oracle / SQL Server / ClickHouse / DM / Doris / StarRocks部署形态Docker docker-compose 一键起学AI大模型的正确顺序千万不要搞错了2026年AI风口已来各行各业的AI渗透肉眼可见超多公司要么转型做AI相关产品要么高薪挖AI技术人才机遇直接摆在眼前有往AI方向发展或者本身有后端编程基础的朋友直接冲AI大模型应用开发转岗超合适就算暂时不打算转岗了解大模型、RAG、Prompt、Agent这些热门概念能上手做简单项目也绝对是求职加分王给大家整理了超全最新的AI大模型应用开发学习清单和资料手把手帮你快速入门学习路线:✅大模型基础认知—大模型核心原理、发展历程、主流模型GPT、文心一言等特点解析✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑✅开发基础能力—Python进阶、API接口调用、大模型开发框架LangChain等实操✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经以上6大模块看似清晰好上手实则每个部分都有扎实的核心内容需要吃透我把大模型的学习全流程已经整理好了抓住AI时代风口轻松解锁职业新可能希望大家都能把握机遇实现薪资/职业跃迁这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】