Agentes de IA (prompts)¶
O system prompt é o único texto que o modelo lê em toda volta do laço, e é
o que menos aparece no código: uma string solta no Agent(...) do primeiro
exemplo, e depois um SERVICE_AGENT_PROMPT importado de uma pasta prompts/
que a página de arquitetura cita sem mostrar.
Esta página mostra a pasta, o que vai dentro de cada arquivo, como compor um prompt que depende de quem pergunta, e como testar que ele chegou ao modelo.
Antes desta página
Agentes de IA até Ferramentas tipadas com Pydantic, e
Agentes de IA (arquitetura) para o layout src/ai/
onde a pasta prompts/ mora.
Nenhum exemplo daqui carrega modelo: todos rodam com o ScriptedBackend de
Agentes de IA (testes), que guarda o que o agente mandou
em cada chamada.
O que o modelo recebe¶
Antes de escrever prompt, veja o que de fato chega ao backend. São três
coisas, e só uma delas é o seu system_prompt:
import asyncio
import json
from pydantic import Field
from tempest_fastapi_sdk.agents import Agent, AgentContext, Skill, tool
from tempest_fastapi_sdk.agents.testing import ScriptedBackend, replies
from tempest_fastapi_sdk.schemas import BaseSchema
class SearchArgs(BaseSchema):
"""Arguments for the catalogue search."""
name: str = Field(description="Partial match on the service title.")
@tool("search_services", "Search the public service catalogue by name.")
async def search_services(args: SearchArgs, context: AgentContext) -> str:
"""Return a canned catalogue line."""
return f"1 serviço: {args.name} | Picos/PI"
invoicing = Skill(
name="invoicing",
description="Ler e validar notas fiscais (NF-e).",
instructions="O guia completo da NF-e.",
)
async def main() -> None:
"""Run once against a scripted backend and print what it received."""
backend = ScriptedBackend([replies("ok")])
agent = Agent(
backend,
tools=[search_services],
skills=[invoicing],
system_prompt="Você é o assistente do catálogo de serviços.",
)
await agent.run("Tem eletricista em Picos?")
print("--- system")
print(backend.system_prompts[0])
print("--- user")
print(backend.prompts[0])
print("--- tools")
print(backend.specs_seen[0])
print(json.dumps(search_services.to_spec(), ensure_ascii=False, indent=2))
if __name__ == "__main__":
asyncio.run(main())
--- system
Você é o assistente do catálogo de serviços.
You have skills available. Each is a set of instructions and tools you can load with the 'load_skill' tool when the task needs it:
- invoicing: Ler e validar notas fiscais (NF-e).
Load a skill before doing work that falls under it. Do not load skills you do not need.
--- user
Tem eletricista em Picos?
--- tools
['search_services', 'load_skill']
{
"type": "function",
"function": {
"name": "search_services",
"description": "Search the public service catalogue by name.",
"parameters": {
"description": "Arguments for the catalogue search.",
"properties": {
"name": {
"description": "Partial match on the service title.",
"title": "Name",
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
}
}
}
Pedaço por pedaço:
systemé o seusystem_prompt, mais o bloco que o SDK anexa quando o agente tem skills. Você não escreve esse bloco, mas ele está lá — e é por isso que vale olhar a saída real em vez de presumir.useré o objetivo passado arun(). Muda a cada execução; o prompt não.- As ferramentas viajam fora do prompt, como specs: nome, a
descriptiondo@toole o JSON-schema dos argumentos. Repare que até a docstring deSearchArgschegou ao modelo, comodescriptiondos parâmetros.load_skillaparece porque o agente tem uma skill.
A consequência prática da terceira linha: o prompt não precisa repetir o schema. O modelo já recebe nome, tipo e descrição de cada argumento. O que o prompt acrescenta é o que o schema não diz — quando usar cada ferramenta.
O system é o mesmo em todas as voltas
A mensagem system é montada uma vez por execução e reenviada sem mudança
em cada chamada ao modelo. Medido com uma execução de três voltas: os três
backend.system_prompts eram idênticos. Por isso dado que muda no meio da
execução não pertence ao prompt (mais sobre isso
abaixo).
A pasta prompts/¶
Um prompt por agente, e um arquivo com as regras que todos compartilham:
src/ai/prompts/
├── __init__.py # re-exporta as constantes
├── base.py # regras comuns a todo agente
├── service.py # SERVICE_AGENT_PROMPT
└── support.py # SUPPORT_AGENT_PROMPT
"""Text blocks every agent in this service shares."""
TOOL_FAILURE_RULE: str = (
"Quando uma ferramenta falhar, leia o erro e mude a chamada; "
"não repita a mesma chamada."
)
NO_GUESSING_RULE: str = (
"Responda só com o que as ferramentas devolveram. "
"Se elas não trouxerem a resposta, diga que não encontrou."
)
ANSWER_FORMAT_RULE: str = "Responda em português, em até três frases, sem markdown."
BASE_RULES: str = "\n".join(
[TOOL_FAILURE_RULE, NO_GUESSING_RULE, ANSWER_FORMAT_RULE],
)
"""System prompt of the service-catalogue agent."""
from src.ai.prompts.base import BASE_RULES
SERVICE_AGENT_PROMPT: str = f"""\
Você é o assistente do catálogo de serviços da plataforma.
Escopo: o catálogo público de serviços e os serviços que o próprio usuário publicou.
Para qualquer outro assunto, diga que não pode ajudar com isso.
Ferramentas:
- search_services: perguntas sobre o catálogo público ("tem eletricista em Picos?").
- get_my_services: perguntas sobre o que o próprio usuário publicou.
{BASE_RULES}"""
"""System prompt of the support agent."""
from src.ai.prompts.base import BASE_RULES
SUPPORT_AGENT_PROMPT: str = f"""\
Você é o atendimento da plataforma.
Escopo: dúvidas de conta, pagamento e uso do site.
Para dúvidas sobre o catálogo de serviços, diga que o assistente do catálogo responde isso.
Ferramentas:
- search_help: procure no FAQ antes de responder qualquer dúvida de uso.
- open_ticket: só quando o FAQ não resolver e o usuário pedir atendimento humano.
{BASE_RULES}"""
from src.ai.prompts.base import (
ANSWER_FORMAT_RULE,
BASE_RULES,
NO_GUESSING_RULE,
TOOL_FAILURE_RULE,
)
from src.ai.prompts.service import SERVICE_AGENT_PROMPT
from src.ai.prompts.support import SUPPORT_AGENT_PROMPT
__all__: list[str] = [
"ANSWER_FORMAT_RULE",
"BASE_RULES",
"NO_GUESSING_RULE",
"SERVICE_AGENT_PROMPT",
"SUPPORT_AGENT_PROMPT",
"TOOL_FAILURE_RULE",
]
O que cada escolha compra:
- Constante de módulo,
UPPER_SNAKE_CASE. O prompt é configuração, não lógica: importarSERVICE_AGENT_PROMPTé o quesrc/ai/agents/service.pyfaz, e um teste importa a mesma constante para afirmar sobre ela. base.pyseparado. A regra de falha de ferramenta escrita uma vez vale para os dois agentes; corrigida uma vez, corrige os dois. O teste garante que nenhum prompt esqueceu de incluí-la.__init__.pyre-exporta. O resto do serviço importafrom src.ai.prompts import SERVICE_AGENT_PROMPT, nunca do submódulo — a mesma regra de import de todo pacote do serviço.- Texto do prompt no arquivo, não no banco. Mudar prompt muda comportamento; no arquivo, a mudança passa por revisão e por teste como qualquer outra.
Para ver o resultado montado, rode ao lado de src/:
import asyncio
from tempest_fastapi_sdk.agents import Agent
from tempest_fastapi_sdk.agents.testing import ScriptedBackend, replies
from src.ai.prompts import SERVICE_AGENT_PROMPT
async def main() -> None:
"""Print the system message exactly as the backend receives it."""
backend = ScriptedBackend([replies("ok")])
await Agent(backend, system_prompt=SERVICE_AGENT_PROMPT).run("oi")
print(backend.system_prompts[0])
if __name__ == "__main__":
asyncio.run(main())
Você é o assistente do catálogo de serviços da plataforma.
Escopo: o catálogo público de serviços e os serviços que o próprio usuário publicou.
Para qualquer outro assunto, diga que não pode ajudar com isso.
Ferramentas:
- search_services: perguntas sobre o catálogo público ("tem eletricista em Picos?").
- get_my_services: perguntas sobre o que o próprio usuário publicou.
Quando uma ferramenta falhar, leia o erro e mude a chamada; não repita a mesma chamada.
Responda só com o que as ferramentas devolveram. Se elas não trouxerem a resposta, diga que não encontrou.
Responda em português, em até três frases, sem markdown.
Anatomia de um prompt¶
O SERVICE_AGENT_PROMPT acima tem cinco partes. A ordem e a redação são
julgamento de redação, não resultado medido: esta página não comparou a
taxa de acerto de um prompt contra outro com modelo de verdade, e você deve
desconfiar de quem afirma isso sem dizer o modelo e o N.
| Parte | Responde | No exemplo |
|---|---|---|
| Papel | quem o modelo é nesta conversa | "assistente do catálogo de serviços" |
| Escopo | o que está dentro e o que fazer com o resto | "Para qualquer outro assunto, diga que não pode ajudar" |
| Quando usar cada ferramenta | a decisão que o schema não carrega | search_services para o catálogo público, get_my_services para o que é do usuário |
| Falha de ferramenta | o que fazer quando a observação é um erro | TOOL_FAILURE_RULE |
| Formato | o que a resposta final deve parecer | ANSWER_FORMAT_RULE |
Três coisas aqui não são julgamento, são mecânica do SDK:
- O nome da ferramenta no prompt tem que ser o nome real. O modelo chama
pelo nome; um prompt que cita
search_catalogquando a ferramenta ésearch_servicesensina o modelo a pedir uma ferramenta que não existe, e a observação volta comounknown tool 'search_catalog'; available: .... - O erro que o modelo lê é o que a ferramenta escreveu. A regra de falha
só funciona se a mensagem do
AgentToolErrordisser o que mudar — Agentes de IA (ferramentas) mostra mensagens escritas para isso. - Descrição de ferramenta mora no
@tool, não aqui. O prompt diz quando; adescriptione o schema dizem o quê. Escrever o o quê duas vezes é duas descrições que divergem na primeira edição.
O que o SDK já põe no prompt¶
DEFAULT_SYSTEM_PROMPT¶
Agent(...) sem system_prompt= usa a constante DEFAULT_SYSTEM_PROMPT:
You are a capable assistant working towards the user's goal. Use the available tools when they help and answer directly when they do not. When a tool fails, read the error and try a different approach rather than repeating the same call. When you have the answer, reply with it and stop calling tools.
Passar system_prompt= substitui esse texto — não anexa. Medido: com
system_prompt="X", o backend recebe exatamente "X". Se você quer manter
as regras do default e acrescentar as suas, componha:
from tempest_fastapi_sdk.agents import DEFAULT_SYSTEM_PROMPT, Agent
from tempest_fastapi_sdk.agents.testing import ScriptedBackend
SHOP_PROMPT: str = DEFAULT_SYSTEM_PROMPT + "\n\nResponda sempre em português."
def build_agent() -> Agent:
"""Build an agent that keeps the default rules and adds one of its own.
Returns:
Agent: The agent, on a scripted backend for the example.
"""
return Agent(ScriptedBackend([]), system_prompt=SHOP_PROMPT)
O BASE_RULES da pasta prompts/ é a mesma ideia com as regras do seu
serviço no lugar das do SDK.
skills_prompt¶
Com skills=, o agente anexa ao seu prompt o bloco de skills_prompt(skills)
— o que você viu na primeira saída desta página, começando em
You have skills available. O bloco vem depois do seu texto, e cada skill
ocupa uma linha: - nome: descrição. A instrução inteira da skill só chega
quando o modelo chama load_skill
(Skills).
Dois efeitos disso no seu prompt:
- Não liste as skills de novo. O bloco já lista; repetir é a mesma informação duas vezes no contexto, em toda volta.
- Uma frase sua sobre quando carregar cada skill ainda vale, pela mesma razão do "quando usar cada ferramenta" acima.
DEFAULT_CRITIC_PROMPT¶
refine(worker, critic, goal) usa DEFAULT_CRITIC_PROMPT (ou o que você
passar em critic_prompt=). Ele não vai no system do crítico: entra no
começo da mensagem user, seguido de GOAL: e WORK TO REVIEW:. Medido com
um crítico construído com system_prompt="C": o system recebido foi C, e a
mensagem user começava com o texto de DEFAULT_CRITIC_PROMPT. Um prompt
próprio para o crítico precisa continuar pedindo a palavra exata APPROVED
— é por ela que refine decide
(Laço gerar, criticar, revisar).
Prompt que depende da requisição¶
facts_prompt e recall_prompt devolvem blocos de texto para anexar ao
prompt — os fatos do usuário, o que conversas anteriores podem contribuir. Os
dois dependem de quem pergunta, e o Agent recebe o prompt no construtor:
run() não aceita outro. Logo, prompt por requisição é agente por
requisição, montado por uma função:
import asyncio
from tempest_fastapi_sdk.agents import (
Agent,
AgentBackend,
FactStore,
InMemoryFactStore,
facts_prompt,
)
from tempest_fastapi_sdk.agents.testing import ScriptedBackend, replies
from src.ai.prompts import SERVICE_AGENT_PROMPT
def build_agent(generator: AgentBackend, prompt: str) -> Agent:
"""Build the agent of one request around the process-wide generator.
Args:
generator (AgentBackend): The shared backend; it is reused, never copied.
prompt (str): The system prompt composed for this request.
Returns:
Agent: An agent whose prompt belongs to this request only.
"""
return Agent(generator, system_prompt=prompt, name="service-agent")
async def prompt_for(user_id: str, facts: FactStore) -> str:
"""Compose the fixed prompt with what is known about this user.
Args:
user_id (str): The authenticated caller.
facts (FactStore): Where durable facts live.
Returns:
str: The agent prompt plus the user's facts block.
"""
return SERVICE_AGENT_PROMPT + await facts_prompt(facts, subject=user_id)
async def main() -> None:
"""Serve two users with one generator and show each prompt's tail."""
facts = InMemoryFactStore()
await facts.put("cidade", "Picos/PI", subject="ana")
await facts.put("cidade", "Teresina/PI", subject="bruno")
generator = ScriptedBackend([replies("ok"), replies("ok")])
for user_id in ("ana", "bruno"):
agent = build_agent(generator, await prompt_for(user_id, facts))
await agent.run("Tem eletricista perto de mim?")
for prompt in generator.system_prompts:
print(prompt.splitlines()[-2:])
if __name__ == "__main__":
asyncio.run(main())
['What you already know:', '- cidade: Picos/PI']
['What you already know:', '- cidade: Teresina/PI']
Um gerador, dois agentes, dois prompts. O que é caro — os pesos do modelo —
continua em runtime.py, um por processo; o que é construído por requisição
é só o objeto Agent, que guarda referências para o gerador e para as
ferramentas que já existiam.
Quanto custa construir o agente¶
Medido no mesmo i9-13900F sob WSL2 de Quanto custa cada passo, Python 3.11.12, mediana de 5 rodadas:
8 ferramentas + 3 skills: 3.30 µs (N=100000 x 5)
8 ferramentas, sem skills: 1.39 µs (N=100000 x 5)
facts_prompt + construção: 12.11 µs (N=20000 x 5)
O passo de modelo mais barato medido naquela página foi 0,29 s (Ollama,
qwen2.5:0.5b, quente). Construir o agente com oito ferramentas tipadas e três
skills custa ~3,3 µs — cinco ordens de grandeza abaixo de uma única volta do
modelo. As skills dobram o custo porque o construtor monta a ferramenta
load_skill e concatena o bloco de skills_prompt; as ferramentas não são
reconstruídas, só a lista que as referencia.
O script da medição
import asyncio
import statistics
import timeit
from collections.abc import Callable
from pydantic import Field
from tempest_fastapi_sdk.agents import (
Agent,
AgentBudget,
AgentContext,
AgentTool,
InMemoryFactStore,
Skill,
facts_prompt,
tool,
)
from tempest_fastapi_sdk.agents.testing import ScriptedBackend
from tempest_fastapi_sdk.schemas import BaseSchema
ROUNDS: int = 5
class SearchArgs(BaseSchema):
"""Arguments shared by the measured tools."""
query: str = Field(description="What to search for.", max_length=255)
city: str | None = Field(default=None, description="City name.")
page: int = Field(default=1, ge=1)
def make_tool(index: int) -> AgentTool:
"""Build one typed tool, the way a service builds them at import.
Args:
index (int): Suffix that keeps the tool names unique.
Returns:
AgentTool: The tool.
"""
@tool(f"tool_{index}", f"Tool {index}: search one domain.")
async def handler(args: SearchArgs, context: AgentContext) -> str:
"""Echo the query."""
return args.query
return handler
TOOLS: list[AgentTool] = [make_tool(index) for index in range(8)]
SKILLS: list[Skill] = [
Skill(
name=f"skill_{index}",
description=f"Domain {index}.",
instructions="x" * 2000,
tools=[make_tool(100 + index)],
)
for index in range(3)
]
GENERATOR: ScriptedBackend = ScriptedBackend([])
PROMPT: str = "You are the service assistant. " * 20
def build_agent(prompt: str, *, skills: list[Skill]) -> Agent:
"""Build the per-request agent around the shared generator.
Args:
prompt (str): The request's system prompt.
skills (list[Skill]): The skills to attach.
Returns:
Agent: A fresh agent; the generator is shared, not copied.
"""
return Agent(
GENERATOR,
tools=TOOLS,
skills=skills,
system_prompt=prompt,
budget=AgentBudget(max_steps=8, max_seconds=30.0),
)
def median_us(call: Callable[[], object], number: int) -> float:
"""Return the median cost of ``call`` in microseconds over ROUNDS rounds.
Args:
call (Callable[[], object]): What to time.
number (int): Calls per round.
Returns:
float: Median microseconds per call.
"""
rounds = [timeit.timeit(call, number=number) / number * 1e6 for _ in range(ROUNDS)]
return statistics.median(rounds)
def main() -> None:
"""Time the construction with and without skills, and with facts."""
loop = asyncio.new_event_loop()
facts = InMemoryFactStore()
loop.run_until_complete(facts.put("cidade", "Picos/PI", subject="ana"))
async def with_facts() -> Agent:
"""Compose the prompt from the store, then build."""
block = await facts_prompt(facts, subject="ana")
return build_agent(PROMPT + block, skills=SKILLS)
with_skills = median_us(lambda: build_agent(PROMPT, skills=SKILLS), 100_000)
without = median_us(lambda: build_agent(PROMPT, skills=[]), 100_000)
composed = median_us(lambda: loop.run_until_complete(with_facts()), 20_000)
print(f"8 ferramentas + 3 skills: {with_skills:.2f} µs (N=100000 x {ROUNDS})")
print(f"8 ferramentas, sem skills: {without:.2f} µs (N=100000 x {ROUNDS})")
print(f"facts_prompt + construção: {composed:.2f} µs (N=20000 x {ROUNDS})")
loop.close()
if __name__ == "__main__":
main()
Uma segunda execução deu 3,37 / 1,40 / 12,17 µs.
No serviço¶
No layout de arquitetura, a fábrica mora em
src/ai/agents/ e o controller a chama a cada requisição. O run_sink e o
FactStore entram por parâmetro, como lá:
from tempest_fastapi_sdk.agents import Agent, AgentRunSink, FactStore, facts_prompt
from src.ai.prompts import SERVICE_AGENT_PROMPT
from src.ai.runtime import agent_budget, generator
from src.ai.tools import get_my_services, search_services
async def build_service_agent(
user_id: str,
*,
facts: FactStore,
run_sink: AgentRunSink | None = None,
) -> Agent:
"""Build the service agent for one caller.
Args:
user_id (str): The authenticated caller; their facts join the prompt.
facts (FactStore): Where durable facts live.
run_sink (AgentRunSink | None): Where finished runs are recorded.
Returns:
Agent: A fresh agent around the process-wide generator and tools.
"""
prompt = SERVICE_AGENT_PROMPT + await facts_prompt(facts, subject=user_id)
return Agent(
generator,
tools=[search_services, get_my_services],
system_prompt=prompt,
budget=agent_budget(),
run_sink=run_sink,
name="service-agent",
)
from tempest_fastapi_sdk.agents import AgentRunSink, FactStore
from src.ai import build_service_agent, context_for
from src.db.models import UserModel
from src.schemas import AgentAnswerResponseSchema, AgentAskRequestSchema
class AIController:
"""Controller for the agent-backed endpoints."""
def __init__(self, facts: FactStore, run_sink: AgentRunSink | None) -> None:
"""Store the process-wide collaborators.
Args:
facts (FactStore): Where durable facts live.
run_sink (AgentRunSink | None): Where finished runs are recorded.
"""
self.facts: FactStore = facts
self.run_sink: AgentRunSink | None = run_sink
async def ask(
self,
user: UserModel,
data: AgentAskRequestSchema,
) -> AgentAnswerResponseSchema:
"""Answer a question with a prompt built for this caller.
Args:
user (UserModel): The authenticated caller.
data (AgentAskRequestSchema): The question.
Returns:
AgentAnswerResponseSchema: The answer plus what the run did.
"""
agent = await build_service_agent(
str(user.id),
facts=self.facts,
run_sink=self.run_sink,
)
run = await agent.run(data.question, context=context_for(user.id))
return AgentAnswerResponseSchema(
output=run.output,
succeeded=run.succeeded,
stop_reason=str(run.stop_reason),
tool_calls=run.tool_calls,
seconds=run.seconds,
)
Com o router pronto¶
O controller acima é endpoint seu. Quando o que muda por requisição é só o
prompt, o router do SDK resolve: make_agent_router aceita, no lugar do
Agent, uma dependência FastAPI que devolve o Agent da requisição.
import asyncio
from fastapi import Depends, FastAPI, Header
from fastapi.testclient import TestClient
from tempest_fastapi_sdk.agents import (
Agent,
InMemoryAgentRunSink,
InMemoryFactStore,
make_agent_router,
)
from tempest_fastapi_sdk.agents.testing import ScriptedBackend, replies
from prompt_per_request import prompt_for
facts: InMemoryFactStore = InMemoryFactStore()
generator: ScriptedBackend = ScriptedBackend([replies("ok"), replies("ok")])
store: InMemoryAgentRunSink = InMemoryAgentRunSink(max_runs=50)
def current_user(x_user_id: str = Header()) -> str:
"""Return who is calling.
A stand-in for your real authentication dependency: trusting a header
is only acceptable behind a gateway that sets it.
"""
return x_user_id
async def agent_for(user_id: str = Depends(current_user)) -> Agent:
"""Build the agent of one request, with the caller's facts in the prompt."""
prompt = await prompt_for(user_id, facts)
return Agent(generator, system_prompt=prompt, run_sink=store, name="service-agent")
app: FastAPI = FastAPI()
app.include_router(
make_agent_router(agent_for, run_store=store, owner=current_user),
)
def main() -> None:
"""Call the router as two users and show what each run received."""
asyncio.run(facts.put("cidade", "Picos/PI", subject="ana"))
client = TestClient(app)
for user_id in ("ana", "bruno"):
client.post(
"/api/agent/run",
json={"goal": "Tem eletricista perto de mim?"},
headers={"X-User-Id": user_id},
)
for prompt in generator.system_prompts:
print(prompt.splitlines()[-2:])
print([run.owner for run in store.recent()])
if __name__ == "__main__":
main()
['What you already know:', '- cidade: Picos/PI']
['Responda só com o que as ferramentas devolveram. Se elas não trouxerem a resposta, diga que não encontrou.', 'Responda em português, em até três frases, sem markdown.']
['bruno', 'ana']
Ana recebeu o fato dela; Bruno, que não tem nenhum, recebeu o
SERVICE_AGENT_PROMPT puro, terminando nas duas últimas regras de BASE_RULES. As
duas execuções ficaram no store com o dono certo (a mais recente primeiro).
O que vale saber da fábrica:
- É uma dependência como qualquer outra. Pode receber o
Request, declararDepends(...)próprios (o usuário, uma sessão), serdefouasync def. O tipo éAgentDependency. - A dependência de
owner=é resolvida uma vez. A fábrica acima declaraDepends(current_user), a mesma função passada emowner=; o FastAPI guarda o valor durante a requisição, então a fábrica e o registro da execução veem o mesmo usuário. - Só
/rune/run/streamchamam a fábrica.GET /runse o download de artefato leem orun_store, sem construir agente — por isso todo agente que a fábrica monta precisa gravar no mesmo sink (run_sink=store). make_agent_router(agent)continua igual. UmAgentpronto é servido como sempre, a mesma instância em toda requisição.
Quando o endpoint ainda é seu
O router semeia só context.owner. Ferramenta que lê quem chama do
state (o context_for + require_user_id da
arquitetura)
continua pedindo o controller próprio de cima — o prompt por requisição
deixou de ser motivo para isso.
O que não pôr no prompt¶
Identidade e permissão. "O usuário é o 42, só mostre os serviços dele" é
uma frase, e o modelo pode ignorar uma frase. Quem pergunta viaja no
AgentContext e a ferramenta o lê com require_user_id
(policy.py).
Um fato no prompt (cidade: Picos/PI) ajuda o modelo a responder; ele não
restringe o que a ferramenta devolve.
Dado que muda dentro da execução. O system é o mesmo em todas as voltas de uma execução (medido acima). Estoque, saldo, status de um pedido: o que pode mudar enquanto o agente trabalha é resposta de ferramenta, lida no momento em que o modelo precisa.
O schema das ferramentas. Já vai nas specs, como a primeira saída desta página mostrou.
Segredo. O prompt é texto que o modelo lê, e nada no SDK impede o modelo de repeti-lo na resposta. Chave de API e credencial ficam no código da ferramenta.
Testar o prompt¶
O prompt é uma constante, então o teste dele é rápido — e dá para afirmar o
que chegou ao modelo pelo ScriptedBackend:
import pytest
from tempest_fastapi_sdk.agents import InMemoryFactStore
from tempest_fastapi_sdk.agents.testing import ScriptedBackend, replies
from prompt_per_request import build_agent, prompt_for
from src.ai.prompts import BASE_RULES, SERVICE_AGENT_PROMPT, SUPPORT_AGENT_PROMPT
def test_every_agent_prompt_carries_the_base_rules() -> None:
"""A prompt that forgot the shared rules is a drift, caught here."""
assert BASE_RULES in SERVICE_AGENT_PROMPT
assert BASE_RULES in SUPPORT_AGENT_PROMPT
@pytest.mark.asyncio
async def test_each_request_sees_only_its_own_facts() -> None:
"""Ana's facts must not reach the run started for Bruno."""
facts = InMemoryFactStore()
await facts.put("cidade", "Picos/PI", subject="ana")
generator = ScriptedBackend([replies("ok"), replies("ok")])
await build_agent(generator, await prompt_for("ana", facts)).run("oi")
await build_agent(generator, await prompt_for("bruno", facts)).run("oi")
assert "Picos/PI" in generator.system_prompts[0]
assert generator.system_prompts[1] == SERVICE_AGENT_PROMPT
O segundo teste afirma uma coisa que só a saída real mostra: sem fato
nenhum, facts_prompt devolve string vazia e o prompt de Bruno é
exatamente o SERVICE_AGENT_PROMPT, sem cabeçalho órfão.
O que esse teste não responde
Se o modelo segue o prompt — escolhe a ferramenta que o prompt manda, respeita o formato — é pergunta para o modelo de verdade, numa suíte separada e marcada: E o modelo de verdade?
Recapitulando¶
- O modelo recebe três coisas: o system (seu prompt mais o que o SDK
anexa), o objetivo e as specs das ferramentas.
ScriptedBackendmostra as três sem modelo. src/ai/prompts/tem um arquivo por agente e umbase.pycom as regras comuns, em constantesUPPER_SNAKE_CASEre-exportadas no__init__.py.- O prompt diz quando; o
@tooldiz o quê. Nome de ferramenta citado no prompt tem que ser o nome real. system_prompt=substitui oDEFAULT_SYSTEM_PROMPT; componha com ele se quiser manter as regras do SDK. Skills anexam o próprio bloco depois do seu texto.- Prompt por requisição é agente por requisição: uma função monta o
Agentem volta do gerador do processo. Medido, custa ~3,3 µs com oito ferramentas e três skills.make_agent_routeraceita essa função como dependência, então os endpoints prontos continuam servindo. - Identidade, permissão, dado volátil e segredo ficam fora do prompt.
- Teste a constante e o que chegou ao modelo; se o modelo obedece é outra suíte.
Veja também: Agentes de IA (ferramentas) para escrever a
mensagem de erro que a regra de falha manda o modelo ler, e
Agentes de IA (avançado)
para os blocos de facts_prompt e recall_prompt.