Gestion des prompts
Un prompt est le texte que votre agent envoie à un modèle, par exemple un prompt système qui indique à un assistant de support comment traiter les remboursements. Goa-AI traite les prompts comme une partie de votre design :
- Vous déclarez dans le design chaque prompt, son rôle et les arguments qu’il accepte.
goa genvérifie chaque template par rapport à ces arguments et génère une fonction typée qui le rend.- À l’exécution, vous pouvez remplacer le texte d’un prompt pour un client, une session ou tout le monde, sans recompiler ni redéployer.
- Le rendu renvoie les versions exactes des prompts qu’il a utilisées. Attachez-les à la requête au modèle qui envoie le texte, et la trace de cette requête montre quel texte a produit quelle réponse.
Prérequis de version
Les prompts déclarés dans le design (SystemPrompt, Prompt, Template,
Role, Args), les paquets de prompts générés et la validation typée des
remplacements ont été intégrés dans goa.design/goa-ai après v0.90.0. En
attendant la prochaine version étiquetée, épinglez goa.design/goa-ai sur le
commit fb00df97927e61cfa444423e77584b950d753bda (la dépendance de module de
goa-ai sélectionne automatiquement une version compatible de
goa.design/goa/v3) :
go get goa.design/goa-ai@fb00df97927e61cfa444423e77584b950d753bda
Concepts
Prompt de base. Le prompt tel que déclaré dans le design : son ID (par exemple
support.assistant.system), son rôle (system, user, tool ou synthesis),
le texte de son template et les champs que le template peut utiliser. Le runtime
enregistre chaque prompt de base une seule fois, et un prompt de base enregistré
ne change jamais. Sa version est dérivée du texte du template : modifier le texte
dans le design produit donc une nouvelle version.
Remplacement. Un texte de remplacement pour un prompt, enregistré dans un magasin de prompts, qui ne s’applique que dans une portée. Chaque remplacement reçoit sa propre version.
Portée. L’endroit où un remplacement s’applique. Une portée a un ID de session
facultatif et les libellés de votre choix, comme account=acme ou region=eu.
Un remplacement s’applique à une requête lorsque :
- le remplacement n’a pas d’ID de session, ou son ID de session est égal à celui de la requête ; et
- chaque libellé du remplacement figure dans la requête avec la même valeur.
Un remplacement sans session ni libellé s’applique partout.
Quel remplacement l’emporte. Lorsque plusieurs remplacements s’appliquent, le runtime en choisit un :
- Un remplacement avec un ID de session l’emporte sur un remplacement sans session.
- Sinon, le remplacement qui a le plus de libellés l’emporte sur celui qui en a moins.
- Sinon, le remplacement le plus récent l’emporte.
Si aucun remplacement ne s’applique, le runtime rend le prompt de base.
Par exemple, avec ces remplacements enregistrés pour support.assistant.system :
| Portée du remplacement | S’applique à une requête de la session s-42 pour account=acme, region=eu ? |
|---|---|
| sans session, sans libellé | oui |
account=acme | oui |
account=acme, region=eu | oui, et l’emporte : il a le plus de libellés |
account=globex | non : la valeur du libellé diffère |
Déclarer les prompts dans le design
Utilisez SystemPrompt dans Agent pour le prompt système de base de l’agent
(<service>.<agent>.system, rôle system), Prompt dans Agent pour les
prompts propres à l’agent (<service>.<agent>.<name>, rôle par défaut user)
et Prompt dans Service pour les prompts partagés entre agents ou code hors
agent (<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" }}`)
})
})
})
Les templates sont rendus avec le struct d’arguments Go généré
(SystemPromptArgs ou <Name>PromptArgs), de sorte que les expressions
utilisent les noms exacts des champs Go générés ({{ .AccountName }},
{{ .RefundLimit }}). goa gen émet les fonctions de prompt d’agent dans
gen/<service>/agents/<agent>/prompts.go (SystemPromptID, Prompt<Name>ID,
SystemPromptArgs, <Name>PromptArgs, PromptSpecSystem(),
PromptSpec<Name>(), PromptSpecs(), RegisterPrompts, RenderSystemPrompt,
Render<Name>Prompt, RenderSystemPromptWithRegistry et
Render<Name>PromptWithRegistry) et les fonctions de prompt de service dans
gen/<service>/prompts/prompts.go.
Enregistrer les prompts
Le runtime doit connaître le prompt de base avant que quoi que ce soit puisse le rendre.
Les prompts d’agent s’enregistrent avec leur agent. AgentRegistration.PromptSpecs
liste les prompts dont un agent est propriétaire, et Runtime.RegisterAgent les
traite :
- Il vérifie d’abord chaque prompt listé, avant d’enregistrer quoi que ce soit.
Un template qui ne s’analyse pas, un ID en double ou un ID déjà enregistré fait
échouer tout l’appel. C’est aussi le cas d’un template qui utilise un champ que
son
ArgsTypene déclare pas, lorsque le prompt déclareArgsType. - Il enregistre l’agent auprès du moteur de workflow.
- Seulement si cela réussit, il ajoute les prompts à
Runtime.PromptRegistry.
Un prompt qui échoue aux vérifications arrête RegisterAgent avant que l’agent
n’atteigne le moteur de workflow. Construire une valeur d’enregistrement n’a aucun
effet de bord : rien n’est enregistré tant que vous n’appelez pas RegisterAgent.
Les prompts de service appartiennent à un service plutôt qu’à un seul agent, si
bien que plusieurs agents et du code hors agent peuvent les partager. Leur
PromptSpec.AgentID est vide, et vous les enregistrez sur
Runtime.PromptRegistry avec la fonction générée RegisterPrompts du package
de service (ou 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) remplit AgentRegistration.PromptSpecs à
partir du slice généré PromptSpecs() sans modifier le registre, et
RegisterAssistantAgent appelle rt.RegisterAgent(ctx, reg).
Les prompts que vous enregistrez à la main continuent de fonctionner. Construisez
un prompt.PromptSpec et passez-le à Runtime.PromptRegistry.Register, comme
avant. Un spec avec un ArgsType nul accepte des données non typées : la syntaxe
de son template et ses Funcs déclarées sont donc vérifiées, mais pas les noms de
champs d’un struct.
Rendre un prompt et suivre les versions utilisées
Le rendu renvoie le texte et la liste de chaque version de prompt utilisée pour le
construire. Cette liste inclut les prompts intégrés avec
{{ render "<prompt-id>" . }}, dans l’ordre où ils ont été rendus. Placez cette
liste sur la requête au modèle qui utilise le texte :
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,
})
Utilisez content.Refs, et non []prompt.PromptRef{content.Ref}. content.Ref
ne nomme que le prompt de premier niveau et omet les prompts qu’il a composés.
Le runtime n’attache pas les versions de prompt à votre place : chaque requête
porte la liste que son appelant y a placée. Si un planificateur rend un prompt de
classification et un prompt de réponse distinct pour deux appels au modèle dans le
même tour, donnez à chaque requête les Refs de son propre rendu. Le span client
gen_ai.chat enregistre gen_ai.prompt.ids, gen_ai.prompt.versions et
goa_ai.prompt.refs ("<id>@<version>") de cette requête.
Modifier un prompt à l’exécution
Enregistrer un remplacement remplace le texte d’un prompt pour une portée. Le
runtime vérifie le nouveau template avant d’enregistrer quoi que ce soit. Les
prompts générés définissent PromptSpec.ArgsType sur
reflect.TypeFor[<Name>PromptArgs]() lorsque Args est déclaré, ou sur
reflect.TypeFor[struct{}]() lorsque le prompt ne prend aucun argument, de sorte
qu’un template qui utilise un champ inconnu est rejeté au moment de
l’enregistrement au lieu d’échouer plus tard à chaque rendu :
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
}
Réinitialiser un remplacement
La réinitialisation supprime les remplacements d’un prompt à une portée exacte : le rendu suivant revient alors au remplacement suivant qui s’applique, ou au prompt de base.
La réinitialisation est destructive :
- Elle supprime définitivement chaque remplacement enregistré pour ce prompt dont la portée est exactement égale à celle que vous passez : le même ID de session et les mêmes libellés, ni plus ni moins.
- Les remplacements des autres portées restent intacts, y compris les portées plus larges et plus étroites.
Historyne renvoie ensuite que les remplacements restants. La réinitialisation ne conserve aucune trace d’audit de ce qu’elle a supprimé. Si vous en avez besoin, enregistrez ce qu’elle va supprimer avant de réinitialiser.
Par exemple, réinitialiser support.assistant.system à la portée account=acme
supprime les remplacements enregistrés exactement à account=acme. Un
remplacement à account=acme, region=eu reste, de même qu’un remplacement sans
libellé. Une requête pour account=acme, region=eu continue d’utiliser le
remplacement plus étroit. Une requête pour account=acme seul utilise désormais
le remplacement sans libellé.
if err := rt.PromptRegistry.ResetOverride(ctx, assistant.SystemPromptID, prompt.Scope{
Labels: map[string]string{"account": "acme"},
}); err != nil {
return err
}
Stocker les remplacements
Les remplacements résident dans un magasin de prompts que vous passez au runtime
avec runtime.WithPromptStore. Goa-AI en inclut deux :
prompt.NewInMemoryStore(), pour les tests et le développement en un seul processus ;- le magasin MongoDB de
features/prompt/mongo, pour la production.
Sans magasin de prompts, chaque rendu utilise le prompt de base.
Magasins personnalisés
Un prompt.Store personnalisé implémente Reset avec le comportement
destructif à portée exacte décrit ci-dessus. Traitez les clés et valeurs de
libellé comme de simples données : une clé peut contenir des points, et une
valeur peut commencer par $.
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 votre application implémente un prompt.Store personnalisé, ajoutez
Reset(ctx, promptID, scope) afin qu’elle supprime définitivement les
remplacements dont PromptID, Scope.SessionID et Scope.Labels correspondent
exactement à scope (et renvoie nil lorsqu’aucun remplacement n’existe à cette
portée exacte).