Startseite / Artikel / LangChain Skills: Agenten-Expertise bei Bedarf laden statt in einem Prompt

LangChain Skills: Agenten-Expertise bei Bedarf laden statt in einem Prompt

Schluss damit, jede Regel in einen einzigen System-Prompt zu stopfen. So nutzen LangChain-Skills ein load_skill-Tool, State und Middleware, um Anweisungen nur bei Bedarf einzufügen.

2088 Wörter

Früher oder später stößt jeder, der Agenten baut, auf dasselbe Problem. Der Assistent soll vieles gut können, etwa SQL schreiben, E-Mails verfassen und Code debuggen, und es kommt noch mehr dazu. Wo leben all diese Anweisungen? Die verlockende Antwort ist, sie in den System-Prompt zu packen, und diese Antwort fängt an wehzutun, sobald der Agent wächst. Unten sehen Sie zuerst, was schiefgeht, und dann, wie das Skills-Muster in LangChain es behebt, anhand eines kleinen lauffähigen Beispiels.

Das Problem mit einem riesigen Prompt

Angenommen, Sie wollen einen einzigen Assistenten, der SQL schreibt, E-Mails entwirft und beim Debuggen hilft. Der naive Weg ist ein langer System-Prompt:

You are a helpful assistant.

When writing SQL:
- Write queries using only SELECT, INSERT, UPDATE, DELETE
- Always use parameterized queries to prevent SQL injection
- Format SQL keywords in UPPERCASE
- Include brief comments for complex queries
When writing emails:
- Keep emails concise and professional
- Use clear subject lines
- Structure: greeting -> purpose -> details -> call to action -> sign-off
- Adjust tone based on context
When debugging:
- First reproduce the error
- Check logs and error messages
- Isolate the root cause
- Suggest a fix with explanation
...

Drei Bereiche sind noch beherrschbar. Fügen Sie juristische Prüfung, Datenanalyse, Support-Skripte und Code-Review-Standards hinzu, und es entstehen konkrete Probleme.

Aufgeblähter Prompt. Jede Anfrage bezahlt Tokens für die Regeln aller Bereiche, auch für solche, die sie nie nutzt, selbst wenn der Nutzer nur SQL wollte. Sie bekommen langsamere und teurere Aufrufe ohne jeden Vorteil.

Verschwimmende Anweisungen. Wenn sich SQL-, E-Mail- und Debugging-Regeln einen Textblock teilen, muss sich das Modell jedes Mal durch alles hindurcharbeiten. Regeln aus einem Bereich sickern in einen anderen, oder das Modell verliert den Überblick, welche Regel wo gilt.

Es skaliert nicht. Jede neue Spezialisierung bedeutet, einen langen, zunehmend fragilen Prompt zu bearbeiten. Auch das Testen wird schwieriger, denn eine Anpassung, die auf den Debugging-Abschnitt zielt, kann unbemerkt verändern, wie E-Mails geschrieben werden.

Größtenteils irrelevant. Beim Entwerfen einer E-Mail braucht das Modell Ihre Regeln gegen SQL-Injection nicht, trotzdem fahren sie bei jedem Aufruf als totes Gewicht mit.

Kurz gesagt behandelt ein großer Prompt alles Wissen als immer relevant, während jede einzelne Anfrage nur einen Ausschnitt davon braucht.

Die Lösung: Expertise bei Bedarf hereinholen

Das Skills-Muster leiht sich eine Idee aus gutem Softwaredesign namens Progressive Disclosure: nicht alles vorab zeigen und Details erst dann offenlegen, wenn sie gebraucht werden.

Statt eines Prompts mit den Regeln aller Bereiche lebt jeder Bereich separat als Skill. Der Agent startet mit einem schlanken Basis-Prompt und einem einzigen Tool, load_skill. Wenn eine Anfrage Spezialwissen erfordert, ruft der Agent dieses Tool auf, das die passenden Anweisungen holt und sie nur für diesen Zug und diesen Bereich in den Prompt einfügt.

So baut das Beispiel es auf.

1. Skills sind gespeicherte Prompts

SKILLS = {
    "sql_expert": """
You are a SQL expert. Follow these rules:
- Write queries using only SELECT, INSERT, UPDATE, DELETE
- Always use parameterized queries to prevent SQL injection
- Format SQL keywords in UPPERCASE
- Include brief comments for complex queries
""",
    "email_writer": """
You are an email writing specialist. Follow these rules:
...
""",
    "debugger": """
You are a debugging specialist. Follow these rules:
...
""",
}

Es ist ein einfaches Dictionary von Skill-Name zu einem Block Anweisungen, und jeder Eintrag steht für sich. Der Basis-Agent braucht diesen Inhalt nicht im Voraus. Er muss nur wissen, dass es die Skills gibt und wofür jeder ungefähr da ist.

2. Der State merkt sich den aktiven Skill

class SkillState(AgentState):
    loaded_skill: str = ""

Wenn Sie das Handoffs-Muster kennen, kommt Ihnen das bekannt vor. Es ist ein einzelnes State-Feld, das leer beginnt und erst einen Wert bekommt, nachdem der Agent entschieden hat, dass eine Spezialisierung nötig ist.

3. Ein Tool, das den Skill holt und in den State schreibt

@tool
def load_skill(
    skill_name: str,
    runtime: ToolRuntime[None, SkillState],
) -> Command:
    """Load a specialized skill prompt.
      Available skills:
          - sql_expert: SQL query writing and optimization
          - email_writer: Professional email composition
          - debugger: Troubleshooting and debugging
          """
          skill = SKILLS.get(skill_name)
          if not skill:
              return Command(
                  update={
                      "messages": [
                          ToolMessage(
                              content=f"Skill '{skill_name}' not found. Available: {', '.join(SKILLS.keys())}",
                              tool_call_id=runtime.tool_call_id,
                          )
                      ],
                      "loaded_skill": "",
                  },
              )
          return Command(
              update={
                  "messages": [
                      ToolMessage(
                          content=f"Loaded skill: {skill_name}",
                          tool_call_id=runtime.tool_call_id,
                      )
                  ],
                  "loaded_skill": skill,
              },
          )

Drei Details verdienen Aufmerksamkeit:

  • Das Modell entscheidet anhand des Docstrings des Tools, wann und welchen Skill es lädt. Darin sind die verfügbaren Skills mit je einer kurzen Beschreibung aufgeführt. Das ist das einzige Verzeichnis der Spezialisierungen, das der Basis-Prompt mitführt, und es enthält nicht die vollständigen Anweisungen.
  • Fordert das Modell einen Skill an, den es nicht gibt, stürzt das Tool nicht ab. Es antwortet mit einer Nachricht, die das Verfügbare auflistet, damit das Modell es erneut versuchen kann.
  • Bei Erfolg wird der Anweisungstext in loaded_skill gespeichert. Das ist der Moment, in dem Spezialwissen hinzukommt.

4. Middleware bringt den Skill in den Prompt

@wrap_model_call
def inject_skill(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
    skill = request.state.get("loaded_skill", "")
    if skill:
        request = request.override(
            system_prompt=(
                f"You have loaded the following skill. Follow these instructions:\n\n{skill}\n\n"
                f"(If you need a different skill, call load_skill again.)"
            ),
        )
    return handler(request)

Das läuft vor jedem Modellaufruf, wie im Handoffs-Beispiel. Ist ein Skill geladen, schreibt die Middleware den System-Prompt so um, dass er genau diese Anweisungen enthält und sonst nichts. Ist keiner geladen, bleibt der einfache Basis-Prompt unverändert.

Hier wird Progressive Disclosure umgesetzt. SQL-Regeln gelangen erst in den Prompt, wenn sql_expert geladen ist, und E-Mail-Regeln tauchen nie auf, solange email_writer nicht angefordert wurde. Das Modell trägt nie Anweisungen für einen Bereich mit sich, in dem es gerade nicht arbeitet.

5. Alles zusammensetzen

agent = create_agent(
    model=free_llm,
    tools=[load_skill],
    state_schema=SkillState,
    middleware=[inject_skill],
    system_prompt=(
        "You are a helpful assistant with access to specialized skills.\n"
        "When a user asks something in a specific domain:\n"
        "1. Call load_skill to get specialized instructions\n"
        "2. Follow those instructions to answer\n\n"
        "Available skills: sql_expert, email_writer, debugger"
    ),
)

Beachten Sie, wie kurz der Basis-system_prompt ist. Er enthält keine SQL-Regel, kein E-Mail-Format und keinen Debugging-Schritt. Er sagt nur, dass es Skills gibt und wie man sie holt. Genau darum geht es: Der Basis-Prompt bleibt klein und stabil, egal wie viele Skills Sie später hinzufügen.

Ein Durchlauf Schritt für Schritt

response = agent.invoke({
    "messages": [{
        "role": "user",
        "content": "Write a SQL query to find the top 5 customers by total purchase amount."
    }],
})

Folgendes passiert:

  1. Das Modell erhält den Basis-Prompt, der kein Fachwissen enthält.
  2. Es liest die Anfrage, erkennt SQL-Arbeit und ruft load_skill(skill_name="sql_expert") auf.
  3. Im Tool gelingt das Nachschlagen von "sql_expert" in SKILLS, und das zurückgegebene Command schreibt den gesamten Anweisungsblock in loaded_skill.
  4. Der Agent kehrt zum Modell zurück. Die Middleware sieht nun, dass loaded_skill nicht leer ist, und schreibt den System-Prompt mit den Regeln des SQL-Experten um.
  5. Erst jetzt weiß das Modell, dass es Schlüsselwörter großschreiben, injektionsanfällige Muster vermeiden und komplexe Abfragen kommentieren soll.
  6. Es schreibt die Abfrage nach diesen Regeln und antwortet dem Nutzer.

Angenommen, die nächste Nachricht des Nutzers betrifft eine E-Mail. Das Modell ruft load_skill("email_writer") auf, und die E-Mail-Regeln ersetzen die SQL-Regeln vollständig. Nichts wird übernommen.

Was Sie gewinnen

Zurück zu den ursprünglichen Beschwerden.

  • Kein Aufblähen. Der Basis-Prompt umfasst ein paar Zeilen, und Spezialtext erscheint nur, wenn er relevant ist, sodass die meisten Anfragen nicht für Wissen bezahlen, das sie nicht nutzen.
  • Kein Verschwimmen. Jeder Skill ist isoliert, daher können SQL-Formatierungsregeln eine E-Mail nicht unbemerkt prägen, denn die beiden stehen nie gemeinsam im Prompt.
  • Es skaliert. Ein neuer code_reviewer-Skill braucht einen neuen Eintrag in SKILLS und eine Zeile im Docstring des Tools. Der Basis-Agent, die Middleware und die bestehenden Skills bleiben, wie sie sind.
  • Relevantes Wissen zur richtigen Zeit. Das ist der Kernnutzen. Die Aufmerksamkeit des Modells verteilt sich nicht auf ein Dutzend unzusammenhängender Regelwerke, sondern konzentriert sich auf das eine, das für die aktuelle Aufgabe zählt.

Ein mentales Bild

Stellen Sie sich eine Präsenzbibliothek vor statt eines Lehrbuchs, das aus allen Fächern zusammengeklebt wurde. Niemand bekommt mit jeder Frage die ganze Bibliothek. Man bekommt den Katalog, hier also Skill-Namen und kurze Beschreibungen, und leiht das eine Buch aus, das die Frage beantwortet. Alle anderen Bände bleiben im Regal, bis sie gebraucht werden.

Das ist die ganze Idee von Skills in LangChain: den Basis-Prompt schlank halten, beschreiben, welche Expertise es gibt, und den Agenten Details erst dann hereinholen lassen, wenn eine Anfrage sie verlangt.

Das vollständige Skript

"""Skills Demo
===========
Skills are specialized prompts loaded on-demand by the agent (progressive disclosure).
Instead of bloating the main prompt with every possible specialization,
the agent uses a load_skill tool to fetch the right instructions when needed.

Flow:
  Agent gets a request -> calls load_skill("domain")
                      -> skill instructions are saved into state
                      -> middleware injects them into the system prompt
                      -> agent now has specialized knowledge to answer
"""

import os
from dotenv import load_dotenv

from langchain.agents import AgentState, create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.messages import ToolMessage
from langchain.tools import tool, ToolRuntime
from langchain_openai import ChatOpenAI
from langgraph.types import Command
from typing import Callable

load_dotenv()


class SkillState(AgentState):
    loaded_skill: str = ""


SKILLS = {
    "sql_expert": """
You are a SQL expert. Follow these rules:
- Write queries using only SELECT, INSERT, UPDATE, DELETE
- Always use parameterized queries to prevent SQL injection
- Format SQL keywords in UPPERCASE
- Include brief comments for complex queries
""",
    "email_writer": """
You are an email writing specialist. Follow these rules:
- Keep emails concise and professional
- Use clear subject lines
- Structure: greeting -> purpose -> details -> call to action -> sign-off
- Adjust tone based on context: formal for clients, casual for team
""",
    "debugger": """
You are a debugging specialist. Follow these rules:
- First reproduce the error
- Check logs and error messages
- Isolate the root cause
- Suggest a fix with explanation
""",
}


@tool
def load_skill(
    skill_name: str,
    runtime: ToolRuntime[None, SkillState],
) -> Command:
    """Load a specialized skill prompt.

    Available skills:
    - sql_expert: SQL query writing and optimization
    - email_writer: Professional email composition
    - debugger: Troubleshooting and debugging
    """
    skill = SKILLS.get(skill_name)
    if not skill:
        return Command(
            update={
                "messages": [
                    ToolMessage(
                        content=f"Skill '{skill_name}' not found. Available: {', '.join(SKILLS.keys())}",
                        tool_call_id=runtime.tool_call_id,
                    )
                ],
                "loaded_skill": "",
            },
        )
    return Command(
        update={
            "messages": [
                ToolMessage(
                    content=f"Loaded skill: {skill_name}",
                    tool_call_id=runtime.tool_call_id,
                )
            ],
            "loaded_skill": skill,
        },
    )


@wrap_model_call
def inject_skill(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
    skill = request.state.get("loaded_skill", "")
    if skill:
        request = request.override(
            system_prompt=(
                f"You have loaded the following skill. Follow these instructions:\n\n{skill}\n\n"
                f"(If you need a different skill, call load_skill again.)"
            ),
        )
    return handler(request)


free_llm = ChatOpenAI(
    model="nvidia/nemotron-nano-9b-v2:free",
    base_url="https://openrouter.ai/api/v1",
    api_key=os.environ.get("OPENROUTER_API_KEY"),
)

agent = create_agent(
    model=free_llm,
    tools=[load_skill],
    state_schema=SkillState,
    middleware=[inject_skill],
    system_prompt=(
        "You are a helpful assistant with access to specialized skills.\n"
        "When a user asks something in a specific domain:\n"
        "1. Call load_skill to get specialized instructions\n"
        "2. Follow those instructions to answer\n\n"
        "Available skills: sql_expert, email_writer, debugger"
    ),
)


print("=" * 60)
print("Skills Demo - Progressive Disclosure")
print("=" * 60)

response = agent.invoke({
    "messages": [{
        "role": "user",
        "content": "Write a SQL query to find the top 5 customers by total purchase amount."
    }],
})

print("\nFull conversation:")
for msg in response["messages"]:
    role = msg.__class__.__name__.replace("Message", "")
    if hasattr(msg, "content") and msg.content:
        print(f"  [{role}] {msg.content[:300]}")
    elif hasattr(msg, "tool_calls") and msg.tool_calls:
        for tc in msg.tool_calls:
            print(f"  [{role}] Tool: {tc['name']}({tc['args']})")

Selbst erweitern

Ein guter nächster Schritt ist das Kombinieren von Skills, etwa debugger neben sql_expert verfügbar zu halten, um eine kaputte Abfrage zu reparieren. Dafür wären zwei Dinge nötig:

  • Entscheiden, ob loaded_skill einen Skill oder eine Liste aktiver Skills enthält.
  • inject_skill so anpassen, dass es mehrere geladene Skills im Prompt zusammenführt, wenn mehr als einer aktiv ist.

Diese kleine Änderung erlaubt es, Spezialisierungen zu kombinieren, statt nur zwischen ihnen zu wechseln, und das Kernmuster bleibt dasselbe.