Gestión de prompts
Un prompt es el texto que tu agente envía a un modelo, como un prompt de sistema que indica a un asistente de soporte cómo gestionar los reembolsos. Goa-AI trata los prompts como parte de tu diseño:
- Declaras en el diseño cada prompt, su rol y los argumentos que recibe.
goa gencomprueba cada plantilla con esos argumentos y genera una función tipada que la renderiza.- En runtime puedes sustituir el texto de un prompt para un cliente, para una sesión o para todos, sin recompilar ni volver a desplegar.
- El renderizado devuelve las versiones exactas de prompt que usó. Adjúntalas a la solicitud al modelo que envía el texto, y la traza de la solicitud muestra qué texto produjo qué respuesta.
Requisito de versión
Los prompts declarados en el diseño (SystemPrompt, Prompt, Template,
Role, Args), los paquetes de prompts generados y la validación tipada de
overrides se incorporaron en goa.design/goa-ai después de v0.90.0. Hasta la
próxima versión etiquetada, fija goa.design/goa-ai al commit
fb00df97927e61cfa444423e77584b950d753bda (el requisito de módulo de goa-ai
selecciona automáticamente una versión compatible de goa.design/goa/v3):
go get goa.design/goa-ai@fb00df97927e61cfa444423e77584b950d753bda
Conceptos
Prompt base. El prompt tal como se declara en el diseño: su ID (por ejemplo
support.assistant.system), su rol (system, user, tool o synthesis), el
texto de su plantilla y los campos que la plantilla puede usar. El runtime
registra cada prompt base una sola vez, y un prompt base registrado nunca
cambia. Su versión se deriva del texto de la plantilla, de modo que cambiar el
texto en el diseño produce una nueva versión.
Override. Texto de sustitución para un prompt, guardado en un almacén de prompts, que solo se aplica dentro de un scope. Cada override recibe su propia versión.
Scope. Dónde se aplica un override. Un scope tiene un ID de sesión opcional
y las etiquetas que elijas, como account=acme o region=eu. Un override se
aplica a una solicitud cuando:
- el override no tiene ID de sesión, o su ID de sesión es igual al de la solicitud; y
- cada etiqueta del override está en la solicitud con el mismo valor.
Un override sin sesión y sin etiquetas se aplica en todas partes.
Qué override gana. Cuando se aplican varios overrides, el runtime elige uno:
- Un override con ID de sesión gana a uno sin sesión.
- Si no, el override con más etiquetas gana al que tiene menos.
- Si no, gana el override más reciente.
Si no se aplica ningún override, el runtime renderiza el prompt base.
Por ejemplo, con estos overrides guardados para support.assistant.system:
| Scope del override | ¿Se aplica a una solicitud en la sesión s-42 para account=acme, region=eu? |
|---|---|
| sin sesión, sin etiquetas | sí |
account=acme | sí |
account=acme, region=eu | sí, y gana: tiene más etiquetas |
account=globex | no: el valor de la etiqueta es distinto |
Declarar prompts en el diseño
Usa SystemPrompt dentro de Agent para el prompt de sistema base del agente
(<service>.<agent>.system, rol system), Prompt dentro de Agent para los
prompts propios del agente (<service>.<agent>.<name>, rol por defecto user)
y Prompt dentro de Service para los prompts compartidos entre agentes o
código que no es de agentes (<service>.<name>):
var _ = Service("support", func() {
Prompt("shared_safety", func() {
Description("Shared safety rules")
Role("system")
Template("Never disclose internal credentials or personal data.")
})
Agent("assistant", "Answer refund questions", func() {
SystemPrompt(func() {
Description("Refund policy")
Args(func() {
Attribute("account_name", String)
Attribute("refund_limit", Int)
Required("account_name", "refund_limit")
})
Template("You help {{ .AccountName }} customers. " +
"Approve refunds up to ${{ .RefundLimit }}. " +
`{{ render "support.shared_safety" }}`)
})
})
})
Las plantillas se renderizan con el struct de argumentos Go generado
(SystemPromptArgs o <Name>PromptArgs), de modo que las expresiones usan los
nombres exactos de los campos Go generados ({{ .AccountName }},
{{ .RefundLimit }}). goa gen emite los helpers de prompts del agente en
gen/<service>/agents/<agent>/prompts.go (SystemPromptID, Prompt<Name>ID,
SystemPromptArgs, <Name>PromptArgs, PromptSpecSystem(),
PromptSpec<Name>(), PromptSpecs(), RegisterPrompts, RenderSystemPrompt,
Render<Name>Prompt, RenderSystemPromptWithRegistry y
Render<Name>PromptWithRegistry) y los helpers de prompts del servicio en
gen/<service>/prompts/prompts.go.
Registrar prompts
El runtime debe conocer el prompt base de un prompt antes de que nada pueda renderizarlo.
Los prompts de agente se registran con su agente.
AgentRegistration.PromptSpecs enumera los prompts que posee un agente, y
Runtime.RegisterAgent los gestiona:
- Primero comprueba todos los prompts enumerados, antes de registrar nada. Una
plantilla que no se puede analizar, un ID duplicado o un ID ya registrado
hacen fallar toda la llamada. También la hace fallar una plantilla que usa un
campo que su
ArgsTypeno declara, cuando el prompt declaraArgsType. - Registra el agente en el motor de workflows.
- Solo si eso tiene éxito, añade los prompts a
Runtime.PromptRegistry.
Un prompt que no supera las comprobaciones detiene RegisterAgent antes de que
el agente llegue al motor de workflows. Construir un valor de registro no tiene
efectos secundarios: no se registra nada hasta que llamas a RegisterAgent.
Los prompts propiedad de un servicio pertenecen a un servicio y no a un solo
agente, de modo que varios agentes y código que no es de agentes pueden
compartirlos. Su PromptSpec.AgentID está vacío, y los registras en
Runtime.PromptRegistry con el helper RegisterPrompts del paquete de servicio
generado (o Runtime.PromptRegistry.Register):
if err := supportprompts.RegisterPrompts(rt.PromptRegistry); err != nil {
return err
}
if err := assistant.RegisterAssistantAgent(ctx, rt, assistant.AssistantAgentConfig{
Planner: myPlanner,
}); err != nil {
return err
}
NewAssistantRegistration(rt, cfg) rellena AgentRegistration.PromptSpecs a
partir del slice generado PromptSpecs() sin mutar el registro, y
RegisterAssistantAgent llama a rt.RegisterAgent(ctx, reg).
Los prompts que registras a mano siguen funcionando. Construye un
prompt.PromptSpec y pásalo a Runtime.PromptRegistry.Register, como antes.
Una spec con ArgsType nulo acepta datos sin tipar, así que se comprueba la
sintaxis de su plantilla y las Funcs declaradas, pero no los nombres de campos
de un struct.
Renderizar un prompt y registrar lo que se usó
El renderizado devuelve el texto y la lista de todas las versiones de prompt
usadas para construirlo. Esa lista incluye los prompts incorporados con
{{ render "<prompt-id>" . }}, en el orden en que se renderizaron. Pon esa
lista en la solicitud al modelo que usa el texto:
content, err := assistant.RenderSystemPrompt(ctx, input.Agent, assistant.SystemPromptArgs{
AccountName: "Acme",
RefundLimit: 100,
})
if err != nil {
return nil, err
}
system := &model.Message{
Role: model.ConversationRoleSystem,
Parts: []model.Part{model.TextPart{Text: content.Text}},
}
resp, err := modelClient.Complete(ctx, &model.Request{
Messages: append([]*model.Message{system}, input.Messages...),
PromptRefs: content.Refs,
})
Usa content.Refs, no []prompt.PromptRef{content.Ref}. content.Ref solo
nombra el prompt de nivel superior y omite los prompts que compuso.
El runtime no adjunta las versiones de prompt por ti: cada solicitud lleva la
lista que su llamador puso en ella. Si un planificador renderiza un prompt de
clasificación y un prompt de respuesta distinto para dos llamadas al modelo en
el mismo turno, da a cada solicitud los Refs de su propio renderizado. El span
de cliente gen_ai.chat registra gen_ai.prompt.ids, gen_ai.prompt.versions
y goa_ai.prompt.refs ("<id>@<version>") de esa solicitud.
Cambiar un prompt en runtime
Guardar un override sustituye el texto de un prompt para un scope. El runtime
comprueba la nueva plantilla antes de guardar nada. Los prompts generados
asignan PromptSpec.ArgsType a reflect.TypeFor[<Name>PromptArgs]() cuando se
declara Args, o a reflect.TypeFor[struct{}]() cuando el prompt no recibe
argumentos, de modo que una plantilla que usa un campo desconocido se rechaza al
guardarla en lugar de fallar más tarde en cada renderizado:
scope := prompt.Scope{
Labels: map[string]string{"account": "acme"},
}
if err := rt.PromptRegistry.ValidateOverride(assistant.SystemPromptID, newTemplate); err != nil {
return err
}
if err := rt.PromptRegistry.SetOverride(ctx, assistant.SystemPromptID, scope, newTemplate, map[string]string{
"author": "ops@acme.com",
}); err != nil {
return err
}
Restablecer un override
Restablecer elimina los overrides de un prompt en un scope exacto, de modo que el siguiente renderizado vuelve al siguiente override que se aplique, o al prompt base.
El restablecimiento es destructivo:
- Elimina de forma permanente todos los overrides guardados para ese prompt cuyo scope es exactamente igual al que pasas: el mismo ID de sesión y las mismas etiquetas, ni más ni menos.
- Los overrides de otros scopes no se modifican, incluidos los más amplios y los más estrechos.
- Después,
Historydevuelve solo los overrides que quedan. El restablecimiento no conserva un registro de auditoría de lo que eliminó. Si necesitas uno, guarda una copia antes de restablecer.
Por ejemplo, restablecer support.assistant.system en el scope account=acme
elimina los overrides guardados exactamente en account=acme. Un override en
account=acme, region=eu permanece, igual que uno sin etiquetas. Una solicitud
para account=acme, region=eu sigue usando el override más estrecho. Una
solicitud solo para account=acme usa ahora el override sin etiquetas.
if err := rt.PromptRegistry.ResetOverride(ctx, assistant.SystemPromptID, prompt.Scope{
Labels: map[string]string{"account": "acme"},
}); err != nil {
return err
}
Almacenar overrides
Los overrides se guardan en un almacén de prompts que pasas al runtime con
runtime.WithPromptStore. Goa-AI incluye dos:
prompt.NewInMemoryStore(), para pruebas y desarrollo en un solo proceso;- el almacén MongoDB de
features/prompt/mongo, para producción.
Sin un almacén de prompts, cada renderizado usa el prompt base.
Almacenes personalizados
Un prompt.Store personalizado implementa Reset con el comportamiento
destructivo y de scope exacto descrito arriba. Trata las claves y los valores de
las etiquetas como datos simples: una clave puede contener puntos, y un valor
puede empezar por $.
type Store interface {
Resolve(ctx context.Context, promptID Ident, scope Scope) (*Override, error)
Set(ctx context.Context, promptID Ident, scope Scope, template string, metadata map[string]string) error
Reset(ctx context.Context, promptID Ident, scope Scope) error
History(ctx context.Context, promptID Ident) ([]*Override, error)
List(ctx context.Context) ([]*Override, error)
}
Si tu aplicación implementa un prompt.Store personalizado, añade
Reset(ctx, promptID, scope) para que elimine de forma permanente los overrides
cuyo PromptID, Scope.SessionID y Scope.Labels coincidan exactamente con
scope (y devuelva nil cuando no exista ningún override en ese scope exacto).