Module 3: Agents with create_agent
Static and Dynamic System Prompts
Capsule overview
System prompts define who your agent is and how it behaves. They're the first instruction the model receives before it processes any user message — they set the role, the constraints, the response format, and the guidelines for using tools.
In this capsule you'll learn two ways to configure system prompts with create_agent. The first is the static prompt: a fixed string that never changes between runs. The second is the dynamic prompt: a function that takes the current state and builds the prompt at runtime, adapting to context (user's name, permissions, time of day, conversation history).
By the end, you'll know when to use each one, how to write effective prompts for agents, and how to take advantage of prompt caching to cut costs with Anthropic.
The prompt parameter in create_agent
In the previous capsule you built agents with create_agent(model, tools). The third key parameter is prompt — it defines the system message the agent receives before any interaction:
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
@tool
def search_web(query: str) -> str:
"""Search the internet for up-to-date information."""
return f"Results for: {query}"
model = ChatOpenAI(model="gpt-4.1-mini")
tools = [search_web]
agent = create_agent(
model,
tools,
prompt="You are a research assistant. Always search for information before answering."
)
result = agent.invoke({
"messages": [("user", "What is LangGraph?")]
})
print(result["messages"][-1].content)
# Expected output: [The agent's answer, based on the search it ran]
Without prompt, the agent falls back to a generic default system message. With prompt, you control exactly how it behaves.
Static prompts: a fixed string
A static prompt is a string that doesn't change between runs. It's the most direct approach and the one you'll use for most agents. For agents with a specific role, structure the prompt with clear sections:
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
@tool
def search_documentation(query: str) -> str:
"""Search the product's technical documentation."""
docs = {
"installation": "pip install our-sdk>=2.0",
"authentication": "Use your API key in the Authorization: Bearer <key> header",
"rate limits": "100 requests/minute on the free plan, 1000 on pro",
}
for key, value in docs.items():
if key in query.lower():
return value
return "No relevant documentation found."
@tool
def create_ticket(title: str, priority: str) -> str:
"""Create a support ticket. priority: 'low', 'medium', 'high'."""
return f"Ticket created: '{title}' (priority: {priority}) — ID: TK-{hash(title) % 10000}"
model = ChatOpenAI(model="gpt-4.1")
SUPPORT_PROMPT = """You are a technical support agent at ACME Corp.
ROLE:
- You help customers with technical problems in the ACME SDK product.
- You respond in English with a professional but friendly tone.
BEHAVIOR:
- Always search the documentation before answering technical questions.
- If the problem can't be solved with documentation, create a support ticket.
- Never make up technical information — only use what you find in the documentation.
RESPONSE FORMAT:
- Concise answers (3 paragraphs max).
- Use bullet points for solution steps.
- Include the ticket ID if you created one.
CONSTRAINTS:
- You cannot modify user accounts or issue refunds.
- If the customer asks for something outside your scope, tell them who to contact."""
agent = create_agent(model, [search_documentation, create_ticket], prompt=SUPPORT_PROMPT)
result = agent.invoke({
"messages": [("user", "How do I install the SDK?")]
})
print(result["messages"][-1].content)
# Expected output: [Answer with installation instructions pulled from the docs]
The prompt gets injected as a SystemMessage at the start of the conversation. Every time the agent processes a message, this prompt is there.
Writing effective prompts for agents
A good agent system prompt has 4 components. Here's an example that puts all of them together:
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
@tool
def search_web(query: str) -> str:
"""Search the internet for up-to-date information."""
return f"Results for '{query}': [simulated data]"
@tool
def calculate(expression: str) -> str:
"""Evaluate a math expression."""
try:
return str(eval(expression))
except Exception as e:
return f"Error: {e}"
model = ChatOpenAI(model="gpt-4.1-mini")
ANALYST_PROMPT = """You are a data analyst specialized in product metrics.
CONSTRAINTS:
- Don't make predictions without data to back them up.
- Don't invent metrics — if you don't have the number, say so.
- Maximum 5 tools per query.
FORMAT:
- Respond in English.
- Use markdown tables for comparisons.
- Show intermediate calculations when you do numeric analysis.
TOOL USAGE:
- Use search_web to get up-to-date market metrics.
- Use calculate for every calculation — don't do mental arithmetic.
- If you can answer from your own knowledge without searching, do it directly."""
agent = create_agent(model, [search_web, calculate], prompt=ANALYST_PROMPT)
result = agent.invoke({
"messages": [("user", "What's the percentage growth from 150 to 195?")]
})
print(result["messages"][-1].content)
# Expected output: [Answer computing (195-150)/150 * 100 = 30%]
The 4 components are:
- Role — Who the agent is. The more specific, the better: "data analyst specialized in metrics" >> "assistant"
- Constraints — What it must NOT do. Without these, the agent can drift out of its domain
- Format — How to structure responses (language, length, style)
- Tool guidelines — When and how to use each tool. This reinforces the tool descriptions
Tips for effective prompts:
- ✅ Put the most important instructions at the start and the end of the prompt (the model pays more attention there)
- ✅ Be specific: "Answer in 3 paragraphs max" >> "Be brief"
- ❌ Avoid contradictions between the prompt and your tool descriptions
- ❌ Avoid overly long prompts that dilute the key instructions
Dynamic prompts: functions that build prompts
Why you need dynamic prompts
Static prompts are enough when the context doesn't change. But in real applications, the agent needs to adapt:
- ✅ Include the current user's name
- ✅ Change behavior based on permissions or role
- ✅ Adjust instructions based on the time of day
- ✅ Modify the prompt based on the conversation history
A dynamic prompt is a function that takes the current state and returns a string:
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
@tool
def search_web(query: str) -> str:
"""Search the internet for up-to-date information."""
return f"Results for '{query}': [simulated data]"
model = ChatOpenAI(model="gpt-4.1-mini")
def dynamic_prompt(state):
"""Build the prompt from the current state."""
message_count = len(state.get("messages", []))
if message_count <= 2:
style = "Give detailed answers with examples."
else:
style = "Keep it short — this conversation is already several turns deep."
return (
f"You are a research assistant. "
f"This is interaction #{message_count} of this session. "
f"{style} Always respond in English."
)
agent = create_agent(model, [search_web], prompt=dynamic_prompt)
result = agent.invoke({
"messages": [("user", "What is LangChain?")]
})
print(result["messages"][-1].content)
# Expected output: [A detailed answer — it's the first interaction]
Every time the agent processes a message, it calls dynamic_prompt(state) to get an updated system message.
Dynamic prompt with user information
The most common case is personalizing the agent for the current user. Custom fields like user_name and user_role require a state_schema (which you'll see in detail in the next capsule):
from typing import TypedDict, Annotated
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import AnyMessage
from datetime import datetime
import operator
class BankState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
user_name: str
user_role: str
@tool
def get_account_balance(account_id: str) -> str:
"""Look up the balance of a bank account."""
balances = {"ACC-001": "$5,230.00", "ACC-002": "$12,750.50"}
return balances.get(account_id, "Account not found")
@tool
def transfer_money(from_account: str, to_account: str, amount: float) -> str:
"""Transfer money between accounts."""
return f"Transfer of ${amount:.2f} from {from_account} to {to_account} — done"
model = ChatOpenAI(model="gpt-4.1")
def banking_prompt(state):
"""Personalized prompt with the user's data and the current time."""
user_name = state.get("user_name", "customer")
user_role = state.get("user_role", "basic")
hour = datetime.now().hour
greeting = "Good morning" if hour < 12 else ("Good afternoon" if hour < 18 else "Good evening")
base = f"{greeting}, you're serving {user_name}. You are a professional banking assistant. Respond in English."
if user_role == "premium":
base += " This is a premium customer — give them priority service. You can make transfers with no limit."
else:
base += " This is a basic customer. Transfers are capped at $1,000 per operation."
return base
agent = create_agent(model, [get_account_balance, transfer_money], prompt=banking_prompt, state_schema=BankState)
result = agent.invoke({
"messages": [("user", "What's my balance in ACC-001?")],
"user_name": "María García",
"user_role": "premium",
})
print(result["messages"][-1].content)
# Expected output: [Personalized answer with the ACC-001 balance]
The prompt changes completely depending on who the user is and when they hit the system.
Dynamic prompt with conversation history
You can inspect the message history to adjust behavior as the conversation moves along:
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
@tool
def search_docs(query: str) -> str:
"""Search the technical documentation."""
return f"Documentation found for: {query}"
model = ChatOpenAI(model="gpt-4.1-mini")
def adaptive_prompt(state):
"""Adapt the prompt to the conversation history."""
messages = state.get("messages", [])
question_count = sum(1 for m in messages if isinstance(m, HumanMessage))
if question_count == 0:
return "You are a programming tutor. First question from the student — be thorough and welcoming. Give code examples."
elif question_count <= 3:
return "You are a programming tutor. The student already has context. You can be more direct — don't repeat concepts you've already explained."
else:
return "You are a programming tutor. This conversation is several turns deep. Be concise. Suggest a hands-on exercise instead of more theory."
agent = create_agent(model, [search_docs], prompt=adaptive_prompt)
result = agent.invoke({
"messages": [("user", "What is a function in Python?")]
})
print(result["messages"][-1].content)
# Expected output: [A thorough, welcoming answer — first interaction]
Prompt caching with Anthropic
When you use Anthropic models, you can take advantage of prompt caching to cut costs. Long system prompts get cached server-side, and subsequent invocations reuse the cache.
To turn the cache on, pass the prompt as a SystemMessage with the cache header:
from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langchain_core.messages import SystemMessage
@tool
def search_knowledge_base(query: str) -> str:
"""Search the internal knowledge base."""
return f"Result for: {query}"
model = ChatAnthropic(model="claude-sonnet-4-20250514")
long_system_prompt = SystemMessage(
content=(
"You are a technical support expert at ACME Corp. "
"You know all of the product documentation, including: "
"API Reference v3.2, the Integration Guide, the customer FAQ, "
"the Troubleshooting Guide, and the changelog for the last 10 releases. "
"Respond in English. "
"Follow the support protocol: 1) Identify the problem, "
"2) Search the documentation, 3) Give a step-by-step solution, "
"4) Offer to open a ticket if it isn't resolved."
),
additional_kwargs={"cache_control": {"type": "ephemeral"}}
)
agent = create_agent(model, [search_knowledge_base], prompt=long_system_prompt)
result = agent.invoke({
"messages": [("user", "How do I set up OAuth authentication?")]
})
print(result["messages"][-1].content)
# Expected output: [An answer that follows the support protocol]
Prompt caching essentials:
- ✅ Prompts of >=1024 tokens get cached automatically
- ✅ Cuts latency (~80% faster for the prompt) and cost (~90% less for cached tokens)
- ⚠️ Only available with Anthropic models
- ⚠️ The cache expires after 5 minutes without use
Comparison: static vs dynamic
| Criterion | Static prompt | Dynamic prompt |
|---|---|---|
| Definition | Fixed string | Function that returns a string |
| When it changes | Never | Every invocation (based on state) |
| Complexity | Low | Medium |
| Use cases | General-purpose agents | Agents personalized by context |
| Prompt caching | ✅ Always works | ⚠️ Only if the output is stable |
| Debugging | Easy — you always know which prompt was used | Requires logging the generated prompt |
Use a static prompt when:
- ✅ The agent has a fixed role that doesn't change
- ✅ You don't need per-user personalization
- ✅ You want maximum benefit from prompt caching
Use a dynamic prompt when:
- ✅ You need to personalize per user (name, permissions, preferences)
- ✅ Behavior changes with the time, the date, or external context
- ✅ You want to adapt the prompt to the conversation history
Connection to the project
In the Research Agent with Tools (this module's project):
- You'll use a detailed static prompt that defines the researcher role
- The prompt will include tool guidelines so the agent knows when to search vs. when to synthesize
- In Module 4 (Middleware), you'll learn
@dynamic_promptas middleware — a more advanced take on dynamic prompts
Everything you learn here applies directly in Capsule 08.
Troubleshooting
Problem 1: The agent ignores the prompt's instructions
Cause: The prompt is too long, or the instructions contradict the tool descriptions. Fix:
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
# ❌ Tool says "any topic", prompt says "tech only" — contradiction
@tool
def search(query: str) -> str:
"""Search the internet for information on any topic."""
return f"Results: {query}"
# ✅ Tool and prompt aligned
@tool
def search_tech(query: str) -> str:
"""Search for technical information about software, hardware, and programming."""
return f"Technical results: {query}"
model = ChatOpenAI(model="gpt-4.1-mini")
agent = create_agent(
model, [search_tech],
prompt="You are a technical assistant. Use search_tech to look things up. If the question isn't technical, say that you can only help with technology topics."
)
Problem 2: The dynamic prompt doesn't get the custom state fields
Cause: The custom fields aren't defined in state_schema, or they aren't passed to invoke().
Fix: Always define the fields in state_schema with a TypedDict and pass every value in invoke(). Use state.get("field", default) to avoid a KeyError.
Problem 3: The prompt is too long and the agent loses focus
Cause: Prompts with too many instructions dilute the model's attention. Fix: Put the most important instructions at the start and the end of the prompt:
# ❌ Critical instruction buried in the middle
bad_prompt = """You are an assistant. You can answer in several languages.
You have search tools. The weather matters.
NEVER make up data. Use markdown tables. Be friendly."""
# ✅ Critical instruction up front
good_prompt = """NEVER make up data — always use the tools available to you.
You are a research assistant that responds in English.
Format: markdown tables for comparisons. Tone: professional but friendly."""
Problem 4: Prompt caching isn't working
Cause: The prompt is under 1024 tokens, you aren't using an Anthropic model, or cache_control is missing.
Fix: Check that you're using ChatAnthropic, that the SystemMessage content is >=1024 tokens, and that you include additional_kwargs={"cache_control": {"type": "ephemeral"}}.
Exercises
Exercise 1: Static prompt for a cooking assistant (Easy)
Create an agent with a static prompt that turns it into a cooking assistant. It should have a search_recipes tool, and the prompt should define all 4 components (role, constraints, format, tool guidelines).
See solution
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
@tool
def search_recipes(ingredient: str) -> str:
"""Search for recipes that use a specific ingredient."""
recipes = {
"chicken": "Lemon roast chicken (30 min), Teriyaki chicken (25 min)",
"pasta": "Pasta carbonara (20 min), Pesto pasta (15 min)",
"rice": "Rice with vegetables (25 min), Fried rice (20 min)",
}
return recipes.get(ingredient.lower(), f"I couldn't find recipes with '{ingredient}'")
model = ChatOpenAI(model="gpt-4.1-mini")
CHEF_PROMPT = """You are a chef assistant specialized in Latin American and Mediterranean cooking.
CONSTRAINTS:
- Only recommend recipes using the ingredients the user mentions.
- If you can't find recipes, suggest alternative ingredients.
FORMAT:
- Respond in English. Include the prep time.
- Use bullet points for the steps.
TOOL USAGE:
- Use search_recipes to look up recipes by ingredient before recommending anything."""
agent = create_agent(model, [search_recipes], prompt=CHEF_PROMPT)
result = agent.invoke({"messages": [("user", "I have chicken, what can I cook?")]})
print(result["messages"][-1].content)
# Expected output: [Answer with the chicken recipes found via the tool]
What's happening: The prompt follows all 4 components. The tool description and the prompt are aligned — both center on searching by ingredient.
Exercise 2: Dynamic prompt with time of day (Easy)
Create an agent with a dynamic prompt that changes tone with the hour: formal in the morning, casual in the afternoon, brief at night.
See solution
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from datetime import datetime
@tool
def get_news(topic: str) -> str:
"""Get recent news about a topic."""
return f"Latest news on {topic}: [simulated news]"
model = ChatOpenAI(model="gpt-4.1-mini")
def time_aware_prompt(state):
hour = datetime.now().hour
if 6 <= hour < 12:
return "Good morning. You are a news assistant with a formal tone. Present the facts in a structured way. Respond in English."
elif 12 <= hour < 20:
return "Good afternoon! You are a casual news assistant. Summarize conversationally. Respond in English."
else:
return "Good evening. You are a brief news assistant. Key points only. Respond in English."
agent = create_agent(model, [get_news], prompt=time_aware_prompt)
result = agent.invoke({"messages": [("user", "What's going on with AI?")]})
print(result["messages"][-1].content)
# Expected output: [An answer whose tone shifts with the current hour]
What's happening: No custom state_schema needed here, because it only uses datetime.now(), not state data.
Exercise 3: Dynamic prompt with user permissions (Medium)
Create a banking agent with a state_schema that includes user_role ("admin" or "viewer"). The prompt should enable transfers only for admins and refuse them for viewers.
See solution
from typing import TypedDict, Annotated
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import AnyMessage
import operator
class BankState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
user_role: str
@tool
def check_balance(account_id: str) -> str:
"""Look up an account's balance."""
return f"Balance for {account_id}: $5,230.00"
@tool
def make_transfer(from_acc: str, to_acc: str, amount: float) -> str:
"""Make a bank transfer."""
return f"Transfer of ${amount:.2f} from {from_acc} to {to_acc} completed."
model = ChatOpenAI(model="gpt-4.1")
def role_based_prompt(state):
role = state.get("user_role", "viewer")
base = "You are a professional banking assistant. Respond in English."
if role == "admin":
return f"{base} The user is an ADMIN — they can check balances and make transfers. Confirm every operation."
return f"{base} The user is a VIEWER — read-only. If they ask for a transfer, explain that they need admin permissions."
agent = create_agent(model, [check_balance, make_transfer], prompt=role_based_prompt, state_schema=BankState)
result_viewer = agent.invoke({
"messages": [("user", "Transfer $100 from ACC-001 to ACC-002")],
"user_role": "viewer",
})
print("Viewer:", result_viewer["messages"][-1].content)
# Expected output: [Refusal — admin permissions required]
What's happening: The state_schema defines user_role. Same agent, different behavior depending on the role.
Exercise 4: A prompt that adapts to the history (Medium)
Create a tutor agent that changes its style based on how many questions the user has asked: thorough for the first 2, direct for the next few, and a practice suggestion after 5.
See solution
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
@tool
def search_python_docs(topic: str) -> str:
"""Search the Python documentation for a specific topic."""
docs = {
"lists": "Lists are mutable sequences: my_list = [1, 2, 3]",
"functions": "Defined with def: def my_func(arg): return arg * 2",
}
for key, value in docs.items():
if key in topic.lower():
return value
return f"No documentation found for '{topic}'."
model = ChatOpenAI(model="gpt-4.1-mini")
def tutor_prompt(state):
messages = state.get("messages", [])
question_count = sum(1 for m in messages if isinstance(m, HumanMessage))
if question_count <= 2:
return "You are a Python tutor for beginners. Give thorough explanations with analogies and examples. Show the expected output. Respond in English."
elif question_count <= 5:
return "You are a Python tutor. Give direct answers with code. Don't repeat concepts you've already explained. Respond in English."
else:
return "You are a Python tutor. Suggest a hands-on exercise instead of more theory. Give the problem statement and hints, not the solution. Respond in English."
agent = create_agent(model, [search_python_docs], prompt=tutor_prompt)
result = agent.invoke({"messages": [("user", "What are lists in Python?")]})
print(result["messages"][-1].content)
# Expected output: [A thorough explanation with analogies — first question]
What's happening: The HumanMessage counter decides which tutor phase you're in, creating a progressive experience within the session.
Exercise 5: Prompt with SystemMessage for prompt caching (Hard)
Create an agent using ChatAnthropic with a long SystemMessage that has cache_control. The prompt should describe a support role with a detailed protocol.
See solution
from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langchain_core.messages import SystemMessage
@tool
def search_kb(query: str) -> str:
"""Search the product's knowledge base."""
kb = {
"login": "To log in: POST /api/auth/login with {email, password}",
"registration": "To register: POST /api/auth/register with {name, email, password}",
"api key": "Generate API keys in Dashboard > Settings > API Keys",
}
for key, value in kb.items():
if key in query.lower():
return value
return "I couldn't find relevant information."
@tool
def create_support_ticket(title: str, severity: str) -> str:
"""Create a support ticket. severity: 'low', 'medium', 'high', 'critical'."""
return f"Ticket #{abs(hash(title)) % 100000} created — '{title}' (severity: {severity})"
model = ChatAnthropic(model="claude-sonnet-4-20250514")
SUPPORT_PROMPT = SystemMessage(
content="""You are a level 2 technical support agent for DataFlow Pro.
IDENTITY: A support engineer with 5 years of experience. You know the REST API, the web dashboard, and the SDKs (Python, Node.js, Go).
SUPPORT PROTOCOL:
1. IDENTIFY: The problem's category (authentication, API, dashboard, deployment, billing).
2. SEARCH: Check the knowledge base before answering.
3. DIAGNOSE: If the search doesn't resolve it, ask specific questions.
4. RESOLVE: A step-by-step solution, with code where relevant.
5. ESCALATE: If you can't solve it, open a ticket with all the information.
CONSTRAINTS:
- NEVER invent endpoints or configuration that isn't in the KB.
- You cannot modify accounts, change plans, or process refunds.
- Maximum 3 searches per query — if you can't find it, escalate.
FORMAT: English, professional and empathetic tone. Code blocks with syntax highlighting.
At the end of every response, ask whether the problem is resolved.
SEVERITIES: low (general questions), medium (bug with a workaround), high (bug with no workaround), critical (service down).""",
additional_kwargs={"cache_control": {"type": "ephemeral"}}
)
agent = create_agent(model, [search_kb, create_support_ticket], prompt=SUPPORT_PROMPT)
result = agent.invoke({"messages": [("user", "I can't log in, I'm getting a 401 error")]})
print(result["messages"][-1].content)
# Expected output: [An answer following the protocol: searches the KB, gives solution steps]
What's happening: The SystemMessage with cache_control lets Anthropic cache this long prompt. Subsequent invocations will be faster and cheaper.
Summary
In this capsule you learned:
- The
promptparameter increate_agentdefines the agent's system message - Static prompts are fixed strings — ideal for agents with a constant role
- A good prompt has 4 components: role, constraints, format, and tool guidelines
- Dynamic prompts are functions that take
stateand return a string — they adapt to context - You can personalize dynamic prompts with user information, time of day, or history
- Custom state fields require defining a
state_schemawithTypedDict(more on that in the next capsule) - With Anthropic, you can use prompt caching with
SystemMessage+cache_controlto cut costs - The most important instructions belong at the start and the end of the prompt
Next capsule: Agent State and Memory — you'll learn to define custom state schemas, manage conversation history, and make the agent remember information between turns.
Further reading
- Agents Conceptual Guide — How agents work under the hood
- How to create agents — The official create_agent guide
- Anthropic Prompt Caching — Official prompt caching documentation
- System Prompts Best Practices (OpenAI) — A guide to system messages
- LangChain Messages — Message types in LangChain
- ReAct Pattern Paper — The original ReAct paper
- LangGraph State Management — How state works in LangGraph
Module 3 — LangChain & LangGraph: From Chains to Agents