LangChain Skills 模式实战:构建按需加载知识的 SQL 助手
在先前的文章中,我们探讨了如何通过 Deep Agents CLI 模拟 Deep Agent 使用 Skills 的模式。如今,LangChain 已原生支持这一特性,极大地简化了开发流程。本文将带领大家深入体验这一功能,构建一个更智能的 SQL 助手。
构建复杂的 AI Agent 时,开发者往往陷入两难境地:是将所有上下文(数据库表结构、API 文档、业务规则)一次性注入 System Prompt,导致上下文窗口(Context Window)溢出且分散模型注意力?还是选择成本高昂的频繁微调(Fine-tuning)?
**Skills 模式(Skills Pattern)**提供了一条优雅的中间路线。它通过动态加载所需知识,实现了上下文的高效利用。LangChain 对此模式的原生支持,意味着我们可以更轻松地构建具备“按需学习”能力的 Agent。
本文将结合官方文档 Build a SQL assistant with on-demand skills,引导读者从零开始,构建一个支持“按需加载知识”的 SQL Assistant。
1. 核心概念:为何选择 Skills 模式?
传统 SQL Agent 的局限性
在传统的 SQL Agent 架构中,我们通常需要在 System Prompt 中提供完整的 Database Schema。随着业务发展,当表数量扩展到数百张时,这种方式会带来显著问题:
-
Token 消耗巨大:每次对话都携带大量无关的表结构,造成资源浪费。
-
幻觉风险增加:过多的无关干扰信息会降低模型的推理准确性。
-
维护困难:所有业务线的知识紧密耦合,难以独立迭代。
Skills 模式:基于渐进式披露的解决方案
Skills 模式基于**渐进式披露(Progressive Disclosure)**原则,将知识获取过程分层处理:
-
Agent 初始状态:仅掌握有哪些“技能”(Skills)及其简要描述(Description),保持轻量级。
-
运行时加载:当面对具体问题(如“查询库存”)时,Agent 主动调用工具(
load_skill)加载该技能详细的上下文(Schema + Prompt)。 -
执行任务:基于加载的精确上下文,执行具体的任务(如编写并执行 SQL)。
这种模式有效支持了无限扩展和团队解耦,使 Agent 能够适应日益复杂的业务场景。
2. 系统架构设计
本实战项目将构建一个包含两个核心 Skills 的 SQL Assistant,以演示该模式的实际应用:
-
Sales Analytics(销售分析):负责
sales_data表,处理收入统计、订单趋势分析等。 -
Inventory Management(库存管理):负责
inventory_items表,处理库存水平监控、位置查询等。
3. 开发环境搭建
本项目采用 Pythonuv进行高效的依赖管理。
核心依赖安装
uv add langchain langchain-openai langgraph psycopg2-binary python-dotenv langchain-community
PostgreSQL 环境配置
本地启动一个 Postgres 实例,并创建agent_platform数据库。我们提供了setup_db.py脚本来自动初始化表结构和测试数据(详见文末源码)。
4. 核心实现步骤详解### Vaihe 1: Määrittele toimialueen taidot (The Knowledge)
Määrittelemme taidot sanakirjarakenteena, simuloiden latausprosessia tiedostojärjestelmästä tai tietokannasta. Huomaa ero description-kentän (jota agentti käyttää päätöksenteossa) ja content-kentän (varsinainen ladattu yksityiskohtainen konteksti) välillä.
SKILLS = {"sales_analytics": {"description":"Hyödyllinen myyntitulojen ja trendien analysointiin...","content":"""... Taulukon skeema: sales_data ..."" },"inventory_management": {"description":"Hyödyllinen varastotasojen tarkistamiseen...","content":"""... Taulukon skeema: inventory_items ..."" }}
Vaihe 2: Toteuta ydintyökalut (The Capabilities)
Agentti on riippuvainen kahdesta avaintyökalusta tehtävien suorittamiseksi:
-
load_skill(skill_name): Lataa määritetyn taidon tiedot dynaamisesti suorituksen aikana. -
run_sql_query(query): Suorittaa tietyn SQL-lauseen.
Vaihe 3: Järjestä agentin logiikka (The Brain)
Hyödynnä LangGraphia ReAct-agentin rakentamiseen. System Promptilla on tässä ratkaiseva rooli, sillä se ohjaa agenttia noudattamaan tiukasti Identify -> Load -> Query -standarditoimintatapaa (SOP).
system_prompt ="""1. Tunnista asiaankuuluva taito.2. Käytä 'load_skill'-toimintoa saadaksesi skeeman.3. Kirjoita ja suorita SQL käyttämällä 'run_sql_query'-toimintoa....Älä arvaile taulukoiden nimiä. Lataa taito aina ensin."""
5. Suorituskyvyn validointi
Suorittamalla test_agent.py, testasimme kyselyitä kahdella eri toimialueella: myynti ja varasto. Alla on konsolin todellinen tulostusloki, joka osoittaa, kuinka agentti lataa taitoja dynaamisesti kysymyksen perusteella:
Testing Sales Query...Agent calling tools: [{'name': 'load_skill', 'args': {'skill_name': 'sales_analytics'}, 'id': 'call_f270d76b7ce4404cb5f61bf2', 'type': 'tool_call'}]Tool output:Olet myyntianalytiikan asiantuntija.Sinulla on pääsy 'sales_data'-taulukkoon.Taulukon skeema:- id: integer...Agent calling tools: [{'name': 'run_sql_query', 'args': {'query': 'SELECT SUM(amount) as total_revenue FROM sales_data;'}, 'id': 'call_b4f3e686cc7f4f22b3bb9ea7', 'type': 'tool_call'}]Tool output: [(Decimal('730.50'),)]...Agent response: The total revenue is $730.50.Testing Inventory Query...Agent calling tools: [{'name': 'load_skill', 'args': {'skill_name': 'inventory_management'}, 'id': 'call_18c823b2d5064e95a0cfe2e3', 'type': 'tool_call'}]Tool output:Olet varastonhallinnan asiantuntija.Sinulla on pääsy 'inventory_items'-taulukkoon.Taulukon skeema...Agent calling tools: [{'name': 'run_sql_query', 'args': {'query': "SELECT warehouse_location FROM inventory_items WHERE product_name = 'Laptop';"}, 'id': 'call_647ee3a444804bd98a045f00', 'type': 'tool_call'}]Tool output: [('Warehouse A',)]...Agent response: The Laptop is located in **Warehouse A**.## 6. Täydellinen lähdekoodiviittaus\n\nSeuraavassa on projektin täydellinen lähdekoodi, joka sisältää tietokannan alustuskomentosarjan ja Agent-pääohjelman.\n\n### 1. Tietokannan alustus (setup_db.py)\n\n`importpsycopg2frompsycopg2.extensionsimportISOLATION_LEVEL_AUTOCOMMITimportosfromdotenvimportload_dotenvload_dotenv()# Varmista, että tietokantayhteystiedot on määritetty .env-tiedostossaDB_HOST = os.getenv(### 2. Agentin pääohjelma (main.py)
`import os from typing import Annotated, Literal, TypedDict, Union, Dict from dotenv import load_dotenv from langchain_openai import ChatOpenAI from langchain_core.tools import tool from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage from langchain_community.utilities import SQLDatabase from langchain_community.agent_toolkits import SQLDatabaseToolkit from langgraph.graph import StateGraph, START, END, MessagesState from langgraph.prebuilt import ToolNode, tools_condition
load_dotenv()
--- Configuration ---
BASE_URL = os.getenv("BASIC_MODEL_BASE_URL") API_KEY = os.getenv("BASIC_MODEL_API_KEY") MODEL_NAME = os.getenv("BASIC_MODEL_MODEL") DB_URI = f"postgresql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}"
--- Database Setup ---
db = SQLDatabase.from_uri(DB_URI)
--- Skills Definition ---
SKILLS: Dict[str, Dict[str, str]] = { "sales_analytics": { "description": "Hyödyllinen myyntitulojen, trendien ja alueellisen suorituskyvyn analysointiin.", "content": """Olet myyntianalytiikan asiantuntija. Sinulla on pääsy 'sales_data'-tauluun. Taulun rakenne:
- id: integer (pääavain)
- transaction_date: date
- product_id: varchar(50)
- amount: decimal(10, 2)
- region: varchar(50) Yleisiä kyselyitä:
- Kokonaistulot: SUM(amount)
- Tulot alueittain: GROUP BY region
- Myyntitrendi: GROUP BY transaction_date""" }, "inventory_management": { "description": "Hyödyllinen varastotasojen, tuotteiden sijaintien ja varastonhallinnan tarkistamiseen.", "content": """Olet varastonhallinnan asiantuntija. Sinulla on pääsy 'inventory_items' table.Table Schema:- id: integer (primary key)- product_id: varchar(50)- product_name: varchar(100)- stock_count: integer- warehouse_location: varchar(50)Common queries:- Check stock: WHERE product_name = '...'- Low stock: WHERE stock_count < threshold""" }}# --- Tools ---@tooldefload_skill(skill_name: str)-> str:""" Lataa tietyn taidon yksityiskohtaisen kehotteen ja kaavion. Saatavilla olevat taidot: - sales_analytics: Myynnin, tulojen ja tapahtumien analysointiin. - inventory_management: Varasto-, tuote- ja varastokyselyihin. """ skill = SKILLS.get(skill_name)ifnotskill:returnf"Virhe: Taito '{skill_name}' ei löytynyt. Saatavilla olevat taidot:{list(SKILLS.keys())}"returnskill["content"]@tooldefrun_sql_query(query: str)-> str:""" Suorita SQL-kysely tietokantaa vasten. Käytä tätä työkalua VASTA, kun olet ladannut sopivan taidon ymmärtääksesi kaavion. """try:returndb.run(query)exceptExceptionase:returnf"Virhe SQL:n suorittamisessa:{e}"@tooldeflist_tables()-> str:"""Listaa kaikki tietokannassa käytettävissä olevat taulukot."""returnstr(db.get_usable_table_names())tools = [load_skill, run_sql_query, list_tables]# --- Agentin asetus ---llm = ChatOpenAI( base_url=BASE_URL, api_key=API_KEY, model=MODEL_NAME, temperature=0)llm_with_tools = llm.bind_tools(tools)# --- Graafin määrittely ---classAgentState(MessagesState):# Voimme lisätä mukautetun tilan tarvittaessa, mutta MessagesState riittää yksinkertaiseen chattiinpassdefagent_node(state: AgentState): messages = state["messages"] response = llm_with_tools.invoke(messages)return{"messages": [response]}workflow = StateGraph(AgentState)workflow.add_node("agent", agent_node)workflow.add_node(\ke({"messages": messages}) last_msg = final_state["messages"][-1]ifisinstance(last_msg, AIMessage): print(last_msg.content) messages = final_state["messages"]# Update history print("-"*50)exceptExceptionase: print(f"\nError:{e}")break`





