告别巨型系统提示让Agent只在需要时加载对应指令成本更低、扩展性更强小伙伴们你有没有好奇过ChatGPT、Gemini 这些聊天界面为什么能直接生成PPT、Excel文件明明底层跑的只是大语言模型哪来的生成文档的能力这个问题的答案不是模型本身变“聪明”了而是一套更轻量的设计技能Skills——也就是智能体只在需要时才加载的专用指令集。今天我们就用LangChain框架从零搭建一个带技能的智能体看看这个机制怎么落地。先搞懂三个核心概念LangChain是当前主流的LLM应用开发框架支持构建智能体、链、检索管道等多种形态的系统同时封装了模型调用、工具管理、记忆管理等通用能力其中create_agent辅助函数可以把模型、工具集、系统提示快速拼装成可运行的智能体大幅降低开发门槛。在这个基础上中间件Middleware是技能机制的基础它挂在智能体和模型之间每次交互都可以重写请求、检查响应、甚至注入额外工具完全不需要修改智能体的核心逻辑设计思路和Web开发里的HTTP中间件非常相似。而技能Skills就是建立在中间件之上的自包含指令包智能体平时只会看到一份简短的技能列表只有当用户的需求匹配到某个技能时才会通过load_skill工具拉取该技能的完整指令。这比把所有可能的指令都塞进一个巨型系统提示要高效得多——毕竟大模型每次交互都要完整读取系统提示指令越多、成本越高。实战搭建带双技能的文档生成智能体我们这次要做的智能体带两个专属技能一个是生成PPT演示文稿的pptx_builder一个是生成Excel报表的excel_reporter两个技能都以独立的SKILL.md文件形式存在最终会调用真实工具把文件保存到本地。前置准备首先你需要准备这些内容1. 一个OpenAI API密钥也可以替换成其他兼容的模型2. 运行环境用Google Colab或者本地Jupyter Notebook都可以3. 新建一个skills文件夹在里面分别创建两个技能对应的markdown文件定义技能逻辑。图片说明File directory structure for project skills两个技能的核心定义分别如下这里用占位符代替具体代码excel_reporter/SKILL.md的核心逻辑--- name: excel_reporter description: Build an Excel (.xlsx) report from one or more named tables --- You are now a **spreadsheet analyst**. Turn the users request into a clean Excel report. Guidelines: - Organize data into one or more sheets; each sheet is a named table. - First row of each sheet is the header row. - Keep numbers as numbers (not strings) so Excel can sum/format them. - Once youve drafted the data, call the create_excel tool with: - title: workbook file name (no extension) - sheets: a list of {sheet_name: str, headers: list[str], rows: list[list]} - Tell the user the file path once its created.pptx_builder/SKILL.md的核心逻辑--- name: pptx_builder description: Build a PowerPoint (.pptx) deck from a title and a list of slides --- You are now a **presentation specialist**. Turn the users request into a short, well-structured slide deck. Guidelines: - 4-8 slides unless the user asks for more. - Each slide needs a short title and 2-4 concise bullet points (no walls of text). - The first slide is a title slide (title optional subtitle, no bullets). - Pick a theme_color and font_name that fit the topic (e.g. green for eco/sustainability, navy/gray for finance, warm orange for food/hospitality). Dont default to the same colors every time — vary them based on what the deck is about, or honor an explicit request (make it blue, use Georgia). - Once youve drafted the outline, call the create_pptx tool with: - title: deck title - slides: a list of {heading: str, bullets: list[str]} - theme_color: 6-digit hex (no #) used for the title slide background and accent bars - font_name: a font available in PowerPoints defaults, e.g. Calibri, Georgia, Verdana - Tell the user the file path once its created.接下来安装项目需要的依赖其中python-pptx负责生成PPT文件openpyxl负责生成Excel文件!pip install -q langchain langchain-core langchain-openai langgraph python-pptx openpyxl注意python-pptx和openpyxl将分别用于创建PPT和Excel文件。为了避免密钥泄露我们可以在运行时动态输入OpenAI密钥不要把它硬编码到 notebook 里import os from getpass import getpass if not os.environ.get(OPENAI_API_KEY): os.environ[OPENAI_API_KEY] getpass(Enter your OpenAI API key: )实现技能加载机制首先我们需要把skills文件夹下的所有SKILL.md文件加载到内存一开始只给智能体展示每个技能的名称和简介不需要暴露完整内容from pathlib import Path from typing import TypedDict SKILLS_DIR Path(skills) OUTPUT_DIR Path(outputs) OUTPUT_DIR.mkdir(exist_okTrue) class Skill(TypedDict): name: str description: str content: str def _load_skills() - list[Skill]: skills [] for skill_file in sorted(SKILLS_DIR.glob(*/SKILL.md)): text skill_file.read_text() _, front_matter, content text.split(---, 2) name front_matter.split(name:)[1].split(\n)[0].strip() description front_matter.split(description:)[1].split(\n)[0].strip() skills.append(Skill(namename, descriptiondescription, contentcontent.strip())) return skills SKILLS _load_skills() [(s[name], s[description]) for s in SKILLS]加载完成后智能体就能看到一份简短的可用技能列表长这样图片说明List of available software tools and descriptions接下来给智能体提供一个load_skill工具它可以根据技能名称拉取对应技能的完整指令from langchain.tools import tool tool def load_skill(skill_name: str) - str: Load the full instructions for a specialized skill by name. for skill in SKILLS: if skill[name] skill_name: return skill[content] return fUnknown skill {skill_name}. Options: {[s[name] for s in SKILLS]}真正的技能机制通过中间件实现这个中间件负责向智能体声明所有可用技能同时把load_skill工具注入到智能体的工具池里from typing import Callable from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse from langchain.messages import SystemMessage class SkillMiddleware(AgentMiddleware): Injects skill descriptions into the system prompt and exposes load_skill. tools [load_skill] def __init__(self): self.skills_prompt \n.join( f- **{skill[name]}**: {skill[description]} for skill in SKILLS ) def wrap_model_call( self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse], ) - ModelResponse: skills_addendum ( f\n\n## Available Skills\n\n{self.skills_prompt}\n\n Call load_skill with the matching name before generating content for that kind of request. ) new_content list(request.system_message.content_blocks) [ {type: text, text: skills_addendum} ] modified_request request.override( system_messageSystemMessage(contentnew_content) ) return handler(modified_request)实现技能对应的真实工具pptx_builder技能最终会调用一个真实的PPT生成工具这个工具支持自定义主题颜色和字体避免所有生成的演示文稿风格都千篇一律from pptx import Presentation from pptx.dml.color import RGBColor from pptx.util import Emu def _rgb(hex_color: str) - RGBColor: return RGBColor.from_string(hex_color.lstrip(#)) def _tint(color: RGBColor, amount: float) - RGBColor: Lighten an RGBColor toward white by amount (0-1). blend lambda c: int(c (255 - c) * amount) return RGBColor(blend(color[0]), blend(color[1]), blend(color[2])) tool def create_pptx( title: str, slides: list[dict], theme_color: str 1F4E79, font_name: str Calibri, ) - str: Create a styled .pptx deck and save it to outputs. accent _rgb(theme_color) tint _tint(accent, 0.85) prs Presentation() title_layout prs.slide_layouts[0] bullet_layout prs.slide_layouts[1] def style_text(text_frame, colorNone, boldNone): for paragraph in text_frame.paragraphs: for run in paragraph.runs: run.font.name font_name if color is not None: run.font.color.rgb color if bold is not None: run.font.bold bold for i, slide_data in enumerate(slides): heading slide_data.get(heading, ) bullets slide_data.get(bullets, []) if i 0: slide prs.slides.add_slide(title_layout) slide.background.fill.solid() slide.background.fill.fore_color.rgb accent slide.shapes.title.text heading style_text( slide.shapes.title.text_frame, colorRGBColor(0xFF, 0xFF, 0xFF), boldTrue, ) if bullets: slide.placeholders[1].text bullets[0] style_text( slide.placeholders[1].text_frame, colortint, ) else: slide prs.slides.add_slide(bullet_layout) slide.background.fill.solid() slide.background.fill.fore_color.rgb RGBColor( 0xFF, 0xFF, 0xFF ) # Accent bar under the title bar slide.shapes.add_shape( MSO_SHAPE.RECTANGLE, # 1 Emu(0), Emu(0), prs.slide_width, Emu(60000), ) bar.fill.solid() bar.fill.fore_color.rgb accent bar.line.fill.background() bar.shadow.inherit False slide.shapes.title.text heading style_text( slide.shapes.title.text_frame, coloraccent, boldTrue, ) body slide.placeholders[1].text_frame body.clear() for j, bullet in enumerate(bullets): p body.paragraphs[0] if j 0 else body.add_paragraph() p.text bullet style_text( body, colorRGBColor(0x33, 0x33, 0x33), ) file_path OUTPUT_DIR / f{title.replace( , _)}.pptx prs.save(file_path) return ( fSaved deck with {len(slides)} slides f({font_name}, #{theme_color}) to {file_path} )excel_reporter技能对应的工具支持设置表头以及每个工作表的的多行数据满足常规报表的需求from openpyxl import Workbook tool def create_excel(title: str, sheets: list[dict]) - str: Create an .xlsx workbook and save it to outputs. wb Workbook() wb.remove(wb.active) for sheet_data in sheets: ws wb.create_sheet(sheet_data[sheet_name][:31]) # Excel sheet-name limit ws.append(sheet_data[headers]) for row in sheet_data[rows]: ws.append(row) file_path OUTPUT_DIR / f{title.replace( , _)}.xlsx wb.save(file_path) return fSaved workbook with {len(sheets)} sheet(s) to {file_path}组装智能体现在我们把所有组件拼起来两个文档生成工具、一行极简的系统提示剩下的技能加载、指令分发工作都交给SkillMiddleware处理from langchain.agents import create_agent agent create_agent( modelopenai:gpt-4o-mini, tools[create_pptx, create_excel], system_promptYou are a document-generation assistant., middleware[SkillMiddleware()], )测试智能体的技能调用我们先给智能体提一个生成PPT的需求为可复用的咖啡杯创业项目做一份演示文稿。智能体应该会自动匹配到pptx_builder技能拉取完整指令后起草大纲、选择主题最终生成符合要求的PPTresult agent.invoke( { messages: [ { role: user, content: Make a slide pitch deck for a startup that sells eco-friendly reusable coffee cups. Use a green theme and a clean font., } ] } ) print(result[messages][-1].content) Done, I created the pitch deck here: outputs/Eco-Friendly_Reusable_Coffee_Cups_Pitch_Deck.pptx It uses a green theme and a clean font.生成的演示文稿效果如下图片说明Presentation slides for a reusable coffee cup startup接下来我们测试Excel生成能力让智能体做一份面包店的季度财务业绩表result agent.invoke( { messages: [ { role: user, content: Build a spreadsheet tracking Q1-Q4 revenue and expenses for a small bakery, } ] } ) print(result[messages][-1].content) Done, your spreadsheet is ready: outputs/bakery_q1_q4_revenue_expenses.xlsx生成的报表效果如下图片说明Quarterly financial performance table for a bakery关于Skills的常见疑问很多同学会问这个技能机制有没有什么额外成本这里整理三个最常见的问题1.只有LangChain能用Skills吗当然不是其他框架也提供了类似的实现模式甚至不依赖任何框架从零用“工具提示”的组合也能实现技能机制本质是一样的。2.Skills会增加API调用成本吗会的因为load_skill是普通工具调用智能体每次加载技能都会多一次和模型的往返调用不过相比把全量指令塞进系统提示的成本这个开销通常更低。3.一次请求可以同时用多个技能吗可以如果用户的需求横跨多个专业领域智能体可以在同一次运行中多次调用load_skill依次加载需要的技能。最后要提醒的是技能不会让你的智能体更“聪明”但会让它更有条理。通过按需加载详细指令你完全可以用一个智能体实现几十种专业行为既不会让系统提示膨胀也不用为每个任务单独启动子智能体。不妨从第一个简单技能开始尝试慢慢扩展你的智能体能力边界。感谢大家的点赞和关注我们下期见
