English · Español
Lab 03 — Generación de tool call dirigida por máscara¶
Objetivo: cerrar el bucle Fase 30 → Fase 31. Usar
ministruct.JSONSchemaMaskpara restringir la salida de un modelo de modo que emita un tool call JSON válido; parsear el JSON; despachar a través del cliente MCP; observar el resultado. End-to-end.Tiempo estimado: 2–3 horas.
Prereq: Lab 02 hecho (
MCPClientfuncionando) Y el Lab 01 de la Fase 30 hecho (JSONSchemaMaskfuncionando sobre el schema de conjugación).
Qué produces¶
En experiments/31-mask-driven-toolcall/:
demo.py— corre el pipeline completo.transcript.json—{prompt, model_output_str, parsed_tool_call, tool_result}para ~20 prompts de test.results.json—{total_prompts, parse_failures, dispatch_failures, tool_errors}.manifest.json— versiones + seed.
Más un pequeño adaptador en src/miniagent/:
src/miniagent/tool_call.py— puente entre un blob de salida de decodificación restringida y un despacho de tool call.
El pipeline¶
prompt
│
▼
MiniGPT.generate(prompt, mask=JSONSchemaMask(tool_call_schema), temperature=0.7)
│ → "{\"name\":\"conjugate\",\"arguments\":{\"verb\":\"eat\",\"tense\":\"past_simple\",\"person\":\"3sg\"}}"
▼
json.loads(output)
│ → {"name": "conjugate", "arguments": {...}}
▼
MCPClient.call_tool("conjugate", verb="eat", tense="past_simple", person="3sg")
│ → "ate"
▼
result printed; logged to transcript
Cada flecha es testeable. El Lab 03 las cablea juntas.
TODOs¶
Bloque A — el schema de envoltorio de tool call¶
En src/miniagent/tool_call.py, define el JSON Schema para un mensaje de tool call (no el schema de argumentos de la tool — una capa por encima):
-
def build_tool_call_schema(tool_schemas: dict[str, dict]) -> dict:return { "type": "object", "properties": { "name": {"type": "string", "enum": list(tool_schemas.keys())}, "arguments": { # see note below on per-tool argument schemas "oneOf": [ {"type": "object", "properties": {"name": {"const": name}, ...}} for name, schema in tool_schemas.items() ] } }, "required": ["name", "arguments"], "additionalProperties": False } - Reality check. Construir un discriminador
oneOfcompleto es engorroso. Para el lab de la Fase 31, simplifica a una máscara de dos etapas: primero generanamebajo un schema solo-enum; luego re-instancia la máscara con elinput_schemaespecífico de la tool y generaarguments. Esto evitaoneOfenteramente. Documenta la simplificación en el docstring del módulo detool_call.py.
La variante de dos etapas en pseudocódigo:
def generate_tool_call(model, prompt, tool_schemas, *, temperature=0.7) -> dict:
name_schema = {"type": "string", "enum": list(tool_schemas)}
name = json.loads(model.generate(prompt + "\nname=", mask=JSONSchemaMask(name_schema)))
arg_schema = tool_schemas[name]
args_text = model.generate(prompt + f'\nname={name}\narguments=', mask=JSONSchemaMask(arg_schema))
return {"name": name, "arguments": json.loads(args_text)}
Esta es la ruta pedagógica. Un sistema de producción usaría oneOf en una sola pasada — esa es una optimización de Fase 33.
Bloque B — conjunto de prompts¶
En experiments/31-mask-driven-toolcall/prompts.json:
- 20 prompts cortos, cada uno diseñado para elicitar un tool call específico. Ejemplos:
(Cinco de cada tool, redacciones variadas.)
[ {"prompt": "I need the past simple of 'eat' for he/she/it.", "expected_tool": "conjugate", "expected_args": {"verb": "eat", "tense": "past_simple", "person": "3sg"}}, {"prompt": "Is 'go' an irregular verb?", "expected_tool": "lookup_irregular_verb", "expected_args": {"verb": "go"}}, {"prompt": "Translate 'ate' to Spanish.", "expected_tool": "lookup_spanish", "expected_args": {"english_form": "ate"}}, {"prompt": "Does 'he go' agree?", "expected_tool": "check_subject_verb_agreement", "expected_args": {"subject": "he", "verb_form": "go"}} ] - Estos son etiquetas oro para evaluación. No esperamos que el pequeño MiniGPT clave la selección de tools — ese es el trabajo de la Fase 32. Solo necesitamos que el parse tenga éxito.
Bloque C — el bucle del demo¶
En experiments/31-mask-driven-toolcall/demo.py:
- Inicializa:
- Carga MiniGPT (de donde sea que las Fases 26-29 lo dejaron; si el modelo no está listo, mockéalo con un stub de salida fija que emita las etiquetas oro — esto es aceptable para la Fase 31 ya que la ruta decodificación-restringida-más-despacho es lo que estamos testeando, no la calidad del modelo).
JSONSchemaMaskpor tool (cache).with MCPClient(["python", "-m", "miniagent.mcp_server"]) as client: client.initialize(); client.list_tools().- Para cada prompt:
- Genera el name bajo la máscara solo-name.
- Genera los args bajo la máscara por-tool.
parsed = {"name": name, "arguments": json.loads(args_text)}.- Valida que
parsedsea un tool call bien formado (build_tool_call_schemavalida). try: result = client.call_tool(**parsed);except ToolError as e: result = {"error": str(e)}.- Append
{prompt, model_output_str, parsed_tool_call, tool_result}atranscript.json. - Agrega contadores →
results.json.
Bloque D — aserciones¶
La afirmación operacional del lab (de theory/01-function-calling-formats.md §"La cuestión del formato de argumentos"): bajo la máscara, los fallos de parseo son imposibles.
- Aserta
results["parse_failures"] == 0. Si algún prompt produjo JSON no parseable, la máscara está rota — depura elJSONSchemaMaskde la Fase 30, no el despacho. - Aserta que cada
tool_resultes o bien una salida de tool válida (string o dict) o bien un texto deToolError(fallo lógico de la tool, vale — es prerrogativa de la tool). - No assertes la precisión de selección de tools. El modelo es pequeño; la selección de tools es Fase 32 con planificación. Estamos testeando el plumbing, no el cerebro.
Bloque E — medición¶
Tiempos por prompt escritos a timings.json:
-
mask_construction_ms— cuánto se tarda en construirJSONSchemaMaskpor tool. -
generation_ms— tiempo del lado del modelo gastado generando el JSON. -
parse_ms—json.loads(siempre microsegundos). -
dispatch_ms— round-trip a través del cliente MCP al servidor y vuelta. -
total_ms.
Reporta el agregado: media, P50, P95. Esta es la latencia base de la Fase 31; la Fase 33 comparará.
Bloque F — loguea un ejemplo exitoso representativo¶
En transcript.json, la primera entrada debe ser un caso demostrativo limpio para el report de la fase:
{
"prompt": "I need the past simple of 'eat' for he/she/it.",
"model_output_name": "conjugate",
"model_output_args": "{\"verb\":\"eat\",\"tense\":\"past_simple\",\"person\":\"3sg\"}",
"parsed_tool_call": {"name": "conjugate", "arguments": {"verb": "eat", "tense": "past_simple", "person": "3sg"}},
"tool_result": "ate",
"timings_ms": {"mask_construction": 1.4, "generation": 142.7, "parse": 0.1, "dispatch": 8.3, "total": 152.5}
}
Este es el artefacto que va a PHASE_31_REPORT.md.
Restricciones¶
- Sin reentrenamiento del modelo. Usa el MiniGPT que exista en la apertura de la fase. Si no puede hacer tool call (o no existe), usa el stub (nota del Bloque C) y documéntalo.
- Un error de tool está bien. Una generación enmascarada que produce un tool call con JSON válido pero una llamada lógicamente equivocada (p. ej.,
conjugate("be", "past_simple", "3sg")devolviendo"was"cuando el prompt era sobreeat) es un fallo de corrección del modelo, no un fallo del lab. - Sin retries en fallo de parseo. Si
parse_failures > 0, arregla la máscara, no lo tapes con retries.
Condiciones de parada¶
Hecho cuando:
results["parse_failures"] == 0sobre 20 prompts.transcript.jsonexiste con 20 entradas.- Tiempos agregados escritos.
- Un caso de éxito representativo seleccionado y copiado-pegado en el borrador del report de la fase.
Pitfalls¶
- La máscara solo restringe lo que está entre los corchetes. La prosa circundante ("Sure, here you go: {...}") aún corrompe el JSON. O prefija la generación con
{(para que el modelo no tenga que elegir empezar el JSON) o usa un schema exterior que incluya el token de llave inicial en su conjunto de aceptación. El Lab 01 de la Fase 30 debería haber manejado esto; si no, arréglalo ahí. - La generación de dos etapas re-ancla el contexto. Cuando generas
arguments=tras generarname=, el contexto previo del modelo difiere del de una generación de una sola pasada. Esto puede dañar la precisión. Aceptable para la Fase 31; la Fase 33 puede unificar en una sola máscara. oneOfy JSON Schema. Muchos validadores JSON-Schema manejanoneOfde forma diferente. No usamosoneOfen tiempo de decodificación (usamos el atajo de dos etapas). Sí lo usamos enbuild_tool_call_schemapara validación; asegúrate de que tu versión de libreríajsonschemalo soporta (lo hace, Draft7+).- Caching de máscaras. Construir un
JSONSchemaMaskdesde cero en cada llamada es caro. Cachea por(schema_id, tokenizer_id). Esta es la única optimización permitida en el Lab 03 — cualquier otra (p. ej., llamadas en paralelo, batch de peticiones) espera a la Fase 33.
Cuándo consultar solutions/¶
Tras results["parse_failures"] == 0 y tener 20 entradas de transcript limpias. La solución en solutions/03-mask-driven-toolcall-ref.md recorre cómo la implementación de referencia manejó la simplificación de dos etapas y cómo habría sido oneOf si hubiéramos unificado.
Fin de labs de la Fase 31. Siguiente: escribir PHASE_31_REPORT.md, luego abrir la Fase 32 (docs/phase-32-agents/).