当前位置: 首页 > news >正文

从 LangGraph 死循环到 Skill 驱动:我把 Text2SQL 升级成了SKILL模式

unsetunset一、为什么我把 Text2SQL 从 LangGraph 升级成 Skill 模式unsetunset

Aix-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 里同时跑着Text2SqlAgentDeepAgentExcelAgent三条线,"schema 探索"这件事它们都要做一遍,结果每个 Agent 里都抄了一份类似的 Prompt。

所以我动手孵化了一个深度问数模块完全走SKILL模式。

新架构就在agent/deepagent/deep_research_agent.py这一个文件里,核心是create_deep_agent()+ 四个 Skill技能——schema-explorationquery-writingreport-generationfrontend-design。整条 Text2SQL 的能力,不再长在 Python 代码里,而是写在 Markdown 里。

这篇文章就讲这次重写:怎么把七阶段 StateGraph,复合成四个 Skill 文件 + 五把 SQL 小工具 + 一个 DeepAgent


unsetunset二、整体架构:把"流程"交还给 LLMunsetunset

2.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_tablessql_db_schemasql_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(timeout=self.LLM_TIMEOUT, max_tokens=self.LLM_MAX_TOKENS) # 根据数据源类型选择 SQL 工具 if db_enum.connect_type == ConnectType.sqlalchemy: db = SQLDatabase.from_uri(uri, sample_rows_in_table_info=3) sql_tools = SQLDatabaseToolkit(db=db, llm=model).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( model=model, memory=memory, skills=skill_paths, tools=sql_tools, backend=FilesystemBackend(root_dir=current_dir), )

它没有 StateGraph、没有节点定义、没有 state schema——就是把菜谱塞进 memory、把锅铲塞进 tools,剩下的交给 LLM。

整个 Text2SQL 的"流程",从 Python 代码里蒸发了。

深度问数


unsetunset三、关键实现拆解unsetunset

3.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.id

M-Schema 这个格式不是我发明的,但把它写进 Skill 里让 LLM 自己组织——这一步把 Text2SQL 的准确率拉高了肉眼可见的一档。

3.3 query-writing:一次对话生成多条 SQL 的意识

单 SQL 的 Text2SQL 已经被玩烂了。真正的业务问题长这样:

“帮我看下今年的月度销售趋势,为什么 8 月特别高?”

这个问题必须拆成三条 SQL

  • SQL1:月度趋势(折线图)
  • SQL2:8 月按品类分解(柱状图)
  • SQL3:8 月 vs 7 月品类对比(瀑布图)

我把这个"多维度查询意识"写进了query-writing/SKILL.md

## 多维度查询策略| 场景 | 策略 ||------|------|| 趋势+归因分析 | SQL1: 时间维度趋势;SQL2: 分类维度归因 || 综合报告 | SQL1: 汇总 KPI;SQL2: 趋势;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: if"task"in node_name.lower(): return Phase.SUB_AGENT if"REPORT_HTML_START"in content or"REPORT_HTML_END"in content: return Phase.REPORTING ifnot tracker.has_tool_called: return Phase.PLANNING return Phase.EXECUTION

判定规则特别朴素:首次工具调用之前的所有输出都是"思考",之后的是"执行",子代理节点单独标记,HTML 报告标记出现就进报告阶段。

每个阶段切换时,用<details>标签把对应内容包起来:

THINKING_SECTION_OPEN = ( '<details style="margin:8px 0;padding:8px 12px;background:#f8f9fa;' "border-left:3px solid #4a90d9;border-radius:4px;font-size:14px;color:#555" '">\n' '<summary style="cursor:pointer;font-weight:600;color:#333">' "🧠 思考与规划</summary>\n\n")

设计亮点:CSS 全部用行内样式。因为公众号环境会剥掉外部样式表,只认style="..."。这是我踩过的坑——早期版本用了 class,结果在公众号预览里全都变白底黑字,折叠都失效了。

![图 3:建议截前端展开「🧠 思考与规划」折叠区的效果图,左侧蓝色竖条 + 灰底 + 思考文字,再截一张折叠收起的状态做对比]

3.5 astream 多模式流:messages + updates 双通道

这是整个文件最核心的一段——_stream_response方法。

deepagents底层是 LangGraph,astream支持同时订阅两种流:

stream_iter = agent.astream( input={"messages": [HumanMessage(content=query)]}, config=config, stream_mode=["messages", "updates"],)
  • messages模式:token 级流,LLM 每吐一个 token 都推一次,用来实现"打字机效果"
  • updates模式:节点级流,每个 LangGraph 节点执行完整体推一次,用来捕获工具调用和结果

两条流并行来,主循环里分别处理:

while True: mode, chunk = await asyncio.wait_for( stream_anext(), timeout=self.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_idfunction_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"\n```sql\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(), timeout=self.STREAM_KEEPALIVE_INTERVAL )except asyncio.TimeoutError: # 25 秒没新数据就发一个 keepalive 心跳 await response.write( 'data: {"data":{"messageType": "info", "content": ""}, ' '"dataType": "keepalive"}\n\n' ) continue

25 秒一个心跳,Nginx 和浏览器都不会断。

第二、任务总超时 30 分钟

TASK_TIMEOUT = 30 * 60connection_closed = await asyncio.wait_for( self._stream_response(agent, config, query, response, ...), timeout=self.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(f"HTML 报告被截断 - 会话: {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五、最后unsetunset

Aix-DB 现在长这样:

  • 四个 Text2SQL Skill:schema-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%免费

http://www.jsqmd.com/news/695809/

相关文章:

  • 2026宝鸡高端装修设计实测:宝鸡市,宝鸡,渭滨宝鸡装修(核心词),宝鸡靠谱家装公司,排行一览! - 优质品牌商家
  • 2026年比较好的硅酸钙板建材专业公司推荐 - 品牌宣传支持者
  • 差分放大器在高速信号链中的关键作用与设计实践
  • keil未指定 PY32F0 具体芯片型号导致编译报错及无法烧录问题
  • 为什么92%的CVE-2025高危漏洞仍源于C内存错误?——2026年NASA、Linux内核与AUTOSAR联合验证的4类零容忍写法
  • 数据标准:梳理业务主题、对象和事件的粒度应如何把握(干货)
  • 港科大DeepTech 20| AI驱动的自动化智能正畸治疗方案设计系统
  • 2026年儿童防开启包装测试审核应对机构top5排行:reach检测,tds报告,检测认证,玩具检测,优选推荐! - 优质品牌商家
  • 统计学与机器学习:差异、融合与应用实践
  • 为什么92%的C项目仍在用不安全strcpy?2026规范强制迁移路线图,含37个API替换对照表
  • 【AI实战笔记】代码健壮性
  • 高效手机号码定位工具:3分钟实现电话号码地理位置精准查询
  • TailClaude:基于iii引擎与Tailscale的浏览器端Claude Code全功能解决方案
  • XGBoost在macOS上的源码编译与优化指南
  • 保姆级教程:创维E900-S盒子免拆刷机,用ADB命令刷入当贝桌面(附固件包)
  • Qt调试技巧:解决DLL输入点错误指南
  • 嗅觉界面测试标准:面向软件测试从业者的专业指南
  • 专知智库发布全球首个《数字内容资产成熟度认证白皮书》——三维生态模型破解“唯流量论”困境,五级成熟度等级重塑内容价值标尺
  • 低成本智能反射面(IRS)在6G毫米波通信中的设计与性能优化
  • 港科夜闻|香港科大于THE亚洲大学排名2026位列第12位,彰显顶尖亚洲大学地位
  • 2026年雅思集训营排行:写作提升营,出国备考营,口语集训营,名校申请营,听力特训营,封闭训练营,排行一览! - 优质品牌商家
  • Go应用性能监控实战:New Relic集成与gorelic原理详解
  • 避开这3个大坑,你的AIGC自学之路能省下90%时间
  • Claude Agent SDK Demos:从工具调用到智能体架构的实战指南
  • 使用ColumnTransformer优化混合数据预处理
  • 不用C、不用Verilog!用Ada点亮LED,这才是Zynq的“另一种打开方式”
  • 2026年甘肃冷冻库厂家TOP5靠谱排行 适配全场景需求 - 优质品牌商家
  • 如何在按需导入时仅执行目标类的初始化代码
  • Linux线程调度机制解析
  • HotswapAgent与DCEVM:实现Java应用运行时无限类重定义,告别重启开发