Inicio / Artículos / Skills de LangChain: cargar la experiencia del agente bajo demanda, no en un único prompt

Skills de LangChain: cargar la experiencia del agente bajo demanda, no en un único prompt

Deje de meter todas las reglas en un solo system prompt. Vea cómo los skills de LangChain usan una herramienta load_skill, el estado y un middleware para inyectar instrucciones solo cuando hacen falta.

2088 palabras

Tarde o temprano, todo el que construye agentes se topa con el mismo obstáculo. El asistente tiene que ser bueno en muchas cosas, como escribir SQL, redactar correos y depurar código, y vendrán más. ¿Dónde viven todas esas instrucciones? La respuesta tentadora es meterlas en el system prompt, y esa respuesta empieza a doler a medida que el agente crece. A continuación verá qué falla primero y luego cómo el patrón de skills de LangChain lo soluciona, con un pequeño ejemplo ejecutable.

El problema de un prompt gigante

Supongamos que quiere un único asistente que escriba SQL, redacte correos y ayude a depurar. La forma ingenua es un system prompt largo:

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

Tres dominios son manejables. Añada revisión legal, análisis de datos, guiones de soporte y estándares de revisión de código, y aparecen problemas concretos.

Prompt inflado. Cada petición paga tokens por las reglas de todos los dominios, incluidas las que nunca usa, aunque el usuario solo quisiera SQL. Obtiene llamadas más lentas y caras sin ninguna ventaja.

Instrucciones difusas. Con las reglas de SQL, correo y depuración compartiendo un bloque de texto, el modelo tiene que abrirse paso por todo cada vez. Las reglas de un área se filtran a otra, o el modelo pierde la pista de qué regla se aplica dónde.

No escala. Cada nueva especialidad implica editar un prompt largo y cada vez más frágil. Probar también se complica, ya que un ajuste pensado para la sección de depuración puede cambiar sin avisar cómo se escriben los correos.

Casi todo irrelevante. Mientras redacta un correo, el modelo no necesita sus reglas contra la inyección SQL, pero viajan en cada llamada como peso muerto.

En resumen, un prompt grande trata todo el conocimiento como siempre relevante, cuando cada petición concreta solo necesita una parte.

La solución: traer la experiencia bajo demanda

El patrón de skills toma prestada una idea del buen diseño de software llamada progressive disclosure (divulgación progresiva): no mostrarlo todo de entrada y revelar los detalles solo cuando se necesitan.

En lugar de un prompt con las reglas de todos los dominios, cada dominio vive por separado como un skill. El agente empieza con un prompt base ligero y una sola herramienta, load_skill. Cuando una petición requiere conocimiento especializado, el agente llama a esa herramienta, que obtiene las instrucciones correspondientes y las inyecta en el prompt solo para ese turno y ese dominio.

Así lo construye el ejemplo.

1. Los skills son prompts almacenados

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 un diccionario sencillo que va del nombre del skill a un bloque de instrucciones, y cada entrada es independiente. El agente base no necesita ese contenido de antemano. Solo necesita saber que los skills existen y para qué sirve aproximadamente cada uno.

2. El estado recuerda el skill activo

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

Si ha visto el patrón de handoffs, esto le resultará familiar. Es un único campo de estado que empieza vacío y solo recibe un valor después de que el agente decide que hace falta una especialidad.

3. Una herramienta que obtiene el skill y lo escribe en el estado

@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,
              },
          )

Tres detalles merecen atención:

  • El modelo decide cuándo y qué skill cargar leyendo el docstring de la herramienta. En él se enumeran los skills disponibles con una breve descripción de cada uno. Es el único índice de especialidades que lleva el prompt base, y no incluye las instrucciones completas.
  • Si el modelo pide un skill que no existe, la herramienta no falla. Responde con un mensaje que enumera lo disponible para que el modelo pueda reintentar.
  • Si tiene éxito, el texto de las instrucciones se guarda en loaded_skill. Ese es el momento en que entra el conocimiento especializado.

4. El middleware pone el skill en el 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)

Esto se ejecuta antes de cada llamada al modelo, como en el ejemplo de handoffs. Cuando hay un skill cargado, reescribe el system prompt para que contenga esas instrucciones y nada más. Cuando no hay ninguno, el prompt base simple queda tal cual.

Aquí es donde se materializa la divulgación progresiva. Las reglas de SQL solo entran en el prompt cuando se carga sql_expert, y las reglas de correo nunca aparecen a menos que se haya pedido email_writer. El modelo nunca carga instrucciones de un dominio en el que no está trabajando.

5. Juntándolo todo

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"
    ),
)

Fíjese en lo corto que es el system_prompt base. No contiene ninguna regla de SQL, ningún formato de correo ni ningún paso de depuración. Solo dice que existen skills y cómo obtenerlos. Esa es la idea: el prompt base se mantiene pequeño y estable por muchos skills que añada después.

Una ejecución, paso a paso

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

Esto es lo que ocurre:

  1. El modelo recibe el prompt base, que no contiene conocimiento de ningún dominio.
  2. Lee la petición, reconoce trabajo de SQL e invoca load_skill(skill_name="sql_expert").
  3. Dentro de la herramienta, la búsqueda de "sql_expert" en SKILLS tiene éxito, y el Command que devuelve escribe todo el bloque de instrucciones en loaded_skill.
  4. El agente vuelve al modelo. Ahora el middleware ve que loaded_skill no está vacío y reescribe el system prompt con las reglas del experto en SQL.
  5. Solo ahora sabe el modelo que debe usar palabras clave en mayúsculas, evitar patrones propensos a la inyección y comentar las consultas complejas.
  6. Escribe la consulta siguiendo esas reglas y responde al usuario.

Supongamos que el siguiente mensaje del usuario trata de un correo. El modelo llama a load_skill("email_writer"), y las reglas de correo sustituyen por completo a las de SQL. No se arrastra nada.

Lo que gana

Volvamos a las quejas iniciales.

  • Sin inflado. El prompt base ocupa unas pocas líneas, y el texto especializado solo aparece cuando es relevante, así que la mayoría de las peticiones no pagan por conocimiento que no usan.
  • Sin difuminado. Cada skill está aislado, así que las reglas de formato de SQL no pueden influir en un correo sin que se note, porque nunca están juntas en el prompt.
  • Escala. Un nuevo skill code_reviewer requiere una entrada nueva en SKILLS y una línea en el docstring de la herramienta. El agente base, el middleware y los skills existentes se quedan como están.
  • Conocimiento relevante en el momento adecuado. Es el beneficio central. La atención del modelo no se reparte entre una docena de conjuntos de reglas inconexos, sino que se centra en el que importa para la tarea actual.

Una imagen mental

Imagine una biblioteca de consulta en lugar de un libro de texto pegado a partir de todas las asignaturas. Nadie recibe la biblioteca entera con cada pregunta. Se recibe el catálogo, que aquí son los nombres de los skills y descripciones breves, y se toma prestado el único libro que responde a la pregunta. Todos los demás volúmenes se quedan en la estantería hasta que hagan falta.

Esa es toda la idea de los skills en LangChain: mantener el prompt base ligero, describir qué experiencia existe y dejar que el agente traiga los detalles solo cuando una petición los requiere.

El script completo

"""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']})")

Amplíelo usted mismo

Un buen siguiente paso es combinar skills, por ejemplo mantener debugger disponible junto a sql_expert para arreglar una consulta rota. Tendría que hacer dos cosas:

  • Decidir si loaded_skill guarda un skill o una lista de skills activos.
  • Actualizar inject_skill para que combine varios skills cargados en el prompt cuando haya más de uno activo.

Ese pequeño cambio le permite combinar especialidades en lugar de solo alternar entre ellas, y el patrón central sigue siendo el mismo.