Capítulo 22 de 22 · avanzado
Proyecto final: una app de principio a fin
Qué cubre esta sesión
No hay nada nuevo esta semana, y ese es el punto. Una aplicación de IA real no es una técnica nueva — son las técnicas que ya tienes, compuestas con criterio. Así que el proyecto final construye un asistente de soporte al cliente funcional a partir de nada más que piezas de capítulos anteriores: un guardrail de moderación en la entrada, un agent loop que decide qué hacer, tools con grounding que buscan en una base de conocimiento y consultan una orden, e instrucciones que fuerzan cada respuesta hacia lo que las tools realmente devolvieron. Lee el código y lo reconocerás todo. Ensámblalo y tienes algo que podrías shippear.
Ese reconocimiento es el curso entero aterrizando. Cuando empezaste, "construye un bot de soporte con IA" era una petición vaga e intimidante. Ahora se descompone en partes móviles que cada una has construido sola: retrieve, ground, llamar tools, loop, moderar. La habilidad que separa a la gente que shippea de la gente que hace demos es exactamente esta — ver un producto como una composición de piezas conocidas, y saber qué pieza necesita cada requisito.
OpenAI
La aplicación entera es una sola función. Modera la entrada; luego corre un agent loop con grounding sobre dos tools; devuelve la respuesta. Fíjate qué poco pegamento se necesita — las partes fueron diseñadas para componerse.
# Grounding sources as tools: a knowledge base to search and an order system to
# query. The agent decides which it needs.
_KB = [
{"topic": "returns", "text": "Items can be returned within 30 days; opened items get store credit only."},
{"topic": "shipping", "text": "Standard shipping is free over $50 and takes 3-5 business days."},
{"topic": "warranty", "text": "Electronics have a 1-year warranty covering defects, not accidental damage."},
]
_ORDERS = {
"1001": {"status": "shipped", "carrier": "UPS", "eta": "Tuesday"},
"1002": {"status": "processing", "eta": "ships within 24 hours"},
}
def search_kb(query: str) -> dict:
words = [w for w in query.lower().split() if len(w) > 2]
return {"results": [e for e in _KB if any(w in (e["text"] + e["topic"]).lower() for w in words)][:3]}
def order_status(order_id: str) -> dict:
return _ORDERS.get(order_id.strip(), {"error": f"no order {order_id!r}"})
_IMPL = {"search_kb": search_kb, "order_status": order_status}
_TOOLS = [
{"type": "function", "name": "search_kb", "description": "Search help articles (returns, shipping, warranty).",
"parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}},
{"type": "function", "name": "order_status", "description": "Look up an order's status by its id.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]}},
]
Las tools son fuentes de grounding entre las que el agent elige: busca en la base de conocimiento para una pregunta de política, consulta una orden para una pregunta de orden. Las instrucciones lo amarran todo — responde solo desde los resultados de las tools, escala cuando las tools se quedan cortas.
# The whole app in one function: moderate, then run the grounded agent loop.
_GOAL = ("You are a customer support assistant. Use search_kb for policy questions and "
"order_status for order questions. Answer ONLY from tool results; if the tools don't "
"cover it, say you'll escalate to a human. Be warm and brief.")
def _flagged(text: str) -> bool:
return client.moderations.create(model="omni-moderation-latest", input=text).results[0].flagged
def assist(message: str) -> str:
if _flagged(message):
return "[message blocked by moderation]"
input_list = [{"role": "user", "content": message}]
response = None
for _ in range(6):
response = client.responses.create(model=_MODEL, instructions=_GOAL, input=input_list, tools=_TOOLS)
calls = [i for i in response.output if i.type == "function_call"]
if not calls:
break
input_list += response.output
for c in calls:
result = _IMPL[c.name](**json.loads(c.arguments))
input_list.append({"type": "function_call_output", "call_id": c.call_id, "output": json.dumps(result)})
return response.output_text if response else ""
Gemini
El mismo ensamblaje en Gemini. Que se porte con solo las llamadas cambiando es la prueba final del hilo conductor del curso: aprendiste patrones, no un vendor. Múdate a un nuevo proveedor — o a un nuevo modelo que salga el próximo mes — y tu arquitectura sobrevive.
def _flagged(text: str) -> bool:
r = client.models.generate_content(
model=_MODEL, contents=[types.Content(role="user", parts=[types.Part(text=text)])],
config=types.GenerateContentConfig(system_instruction=(
"Reply 'flag' if this text is hateful, harassing, sexual, violent, or dangerous; "
"otherwise reply 'ok'. One word only.")),
)
return "flag" in (r.text or "").lower()
def assist(message: str) -> str:
if _flagged(message):
return "[message blocked by moderation]"
contents = [types.Content(role="user", parts=[types.Part(text=message)])]
response = None
for _ in range(6):
response = client.models.generate_content(model=_MODEL, contents=contents, config=_CONFIG)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = [types.Part.from_function_response(name=fc.name, response=_IMPL[fc.name](**dict(fc.args)))
for fc in response.function_calls]
contents.append(types.Content(role="user", parts=parts))
return (response.text or "") if response else ""
Dos pensamientos de cierre para cuando lleves esto más allá de una demo. Primero, todo lo que aprendiste sobre producción sigue aplicando aquí, todo a la vez: loguea el uso de tokens, mide la calidad con un harness de eval, cachea las partes estables del prompt, vigila la latencia, y modera ambos extremos. Un proyecto final es donde esos hábitos dejan de ser lecciones separadas y se vuelven cómo construyes. Segundo, mantenlo tan simple como el problema lo permita — echa mano de un solo agent antes de un swarm multiagente, de prompting antes de fine-tuning, de long context antes de un pipeline de RAG cuando el documento cabe. Las mejores aplicaciones de IA no son las que tienen más maquinaria; son las donde cada pieza está ahí porque un requisito la exigió.
Ponlo a trabajar
Tres apps de docker-compose bajo code/showcase/<slug>/, cada una componiendo
varios capítulos en una sola feature. Misma rutina: bash bootstrap-secrets.sh,
docker compose up --build, http://localhost:3000.
Showcase 1 — Asistente de soporte
El stack completo: moderación + agent + búsqueda con grounding en la KB + consulta de orden. Pregunta por una orden o una política y míralo elegir una tool, fundamentar su respuesta, y mantenerse seguro — o escalar cuando no puede ayudar. Esta es la arquitectura de referencia para una clase enorme de productos reales.
Showcase 2 — Pipeline de contenido
Orquestación multiagente con una compuerta de seguridad en la salida: el writer hace draft, el critic afila, el writer revisa, la moderación aprueba. Una feature de generación de contenido que es tanto mejor (el paso de crítica) como más segura (la compuerta) que una sola llamada.
Showcase 3 — Preguntas y respuestas sobre documentos
Long context, grounding, y moderación juntos: pega un documento, haz una pregunta, recibe una respuesta anclada en el texto. La feature de "chatea con tu PDF" que lanzó cien startups, en unas cuantas docenas de líneas que entiendes por completo.
Tres productos, cero conceptos nuevos. Eso es el curso.
Córrelo
El README de esta carpeta tiene la versión de Python, la línea de instalación, las
dos variables de entorno, y los comandos exactos. Las keys vienen del .env sin
trackear en la raíz del curso. Los scripts básicos corren el asistente sobre una
pregunta de orden y una pregunta de política para que lo veas elegir tools y
fundamentar respuestas.
Lo que te llevas
Una aplicación de IA real es composición, no invención: el asistente de soporte, el pipeline de contenido, y las preguntas y respuestas sobre documentos del proyecto final no introducen nada nuevo — cablean juntos moderación, agents, tools, retrieval, grounding, y long context, cada uno de una semana anterior. Esa es la habilidad hacia la que el curso entero estuvo construyendo — mirar el requisito de un producto y ver qué pieza conocida necesita, luego ensamblar las piezas con los hábitos de producción (medir, cachear, moderar, loguear) que las mantienen honestas. Y el hilo conductor que sobrevive a cualquier modelo: aprendiste patrones, no un proveedor, así que las arquitecturas que construiste aquí se portan a lo que sea que salga después. Empezaste veinte capítulos atrás con una sola llamada a la API. Ahora puedes construir la cosa a la que esa llamada siempre estuvo llevando. Ve y constrúyela.