How to Fix Vague AI Agent Answers With Prompt and Context Engineering
Build one local AI agent with LangChain and Ollama, then compare baseline, prompt-engineered, and context-engineered answers on a real systemd failure, plus a genuine context window gotcha.
If you have ever asked a local AI agent a question and gotten back a vague, generic answer, the model itself is usually not the problem. Two things decide whether an agent gives you a useful answer: how you ask (prompt engineering) and what information it actually has access to when it answers (context engineering). In this tutorial you will build one small AI agent with LangChain and a local Ollama model, then run the exact same agent three different ways: with a bare-bones question, with a carefully engineered prompt, and with a carefully engineered prompt plus real supporting data. You will see, in real captured output, how much the answer quality changes at each stage, and you will learn a genuine gotcha that catches people who skip straight to the “add more context” step without understanding what is happening underneath.
Table Of Content
- What You Will Accomplish
- Prerequisites
- What Is Prompt Engineering?
- What Is Context Engineering?
- Why They Matter Together
- Step 1: Install Ollama and Pull a Model
- Step 2: Set Up Your Python Environment
- Step 3: Build and Run the Baseline Agent
- Step 4: Add Prompt Engineering
- Step 5: Add Context Engineering
- Diagnosing the Gotcha: Context Window Size
- Step 6: Verify the Diagnosis Was Actually Correct
- Common Mistakes and Gotchas
- Treating a Silent Truncation as a Reasoning Failure
- Confusing the Two Techniques
- Context Pollution
- Trusting a Confident-Sounding Answer Without Verification
- How to Verify It All Worked
- Next Steps
The task the agent will work on is a realistic one for anyone who runs Linux servers: diagnosing why a systemd service failed to start. This is deliberately not a toy example. You will break a real service on a real Linux machine, capture its real failure output, and watch the agent try to diagnose it with and without that output in hand.
What You Will Accomplish
By the end of this tutorial, you will have:
- A working local AI agent built with LangChain v1 and Ollama, running entirely on your own machine with no API costs
- A clear, hands-on understanding of the difference between prompt engineering and context engineering, and why you usually need both
- A real systemd troubleshooting scenario you deliberately broke, diagnosed with AI assistance, and fixed
- Working knowledge of a specific, easy-to-hit Ollama gotcha involving context window size, and how to fix it
Prerequisites
You do not need any prior experience building AI agents. You do need:
- A Linux, macOS, or Windows machine. This tutorial was tested end to end on Ubuntu 26.04 LTS with systemd; the AI agent portion works identically on any OS, but the systemd diagnosis example is Linux-specific
- Python 3.10 or later installed
- About 3 GB of free disk space and 4 GB of free RAM for a small local model (less if you pick a smaller model variant, more is better for speed)
- Basic comfort running commands in a terminal. You do not need prior systemd or LangChain experience; both are explained as you go
- Root or sudo access if you want to follow the systemd portion of the tutorial, since creating and inspecting system services requires elevated privileges
What Is Prompt Engineering?
Prompt engineering is the practice of writing your instructions to a model in a way that helps it produce a more useful answer, without changing the model itself. You are changing how you present the task, not the underlying weights or training.
In practice, prompt engineering usually means one or more of the following:
- Specificity: replacing a vague instruction (“help me with this”) with a precise one (“diagnose why this specific service failed”)
- Role assignment: telling the model what perspective to answer from, for example “You are a senior Linux systems administrator”
- Format anchoring: specifying the exact structure you want the answer in, so it is consistent and easy to act on
- Explicit uncertainty handling: instructing the model to say “I don’t have enough information” instead of guessing when it genuinely cannot know the answer
None of this changes what the model knows. It changes how clearly the model understands what you want back.
What Is Context Engineering?
Context engineering is a related but distinct practice: deciding what information the model gets to see before it answers, how that information is organized, and when it is included. LangChain’s own documentation defines context engineering as “providing the right information and tools in the right format so the LLM can accomplish a task,” and describes it as one of the most important jobs of anyone building AI agents, since a lack of the right context is the most common reason agents behave unreliably.
The prompt is part of the context an agent sees, but it is only part. Context can also include system instructions, retrieved documents, tool outputs, conversation history, logs, error messages, or any other supporting data relevant to the task. A perfectly worded prompt cannot make up for missing information. If you ask even a very capable model to diagnose a failure without showing it the failure, it has to guess, no matter how well you phrase the question.
Why They Matter Together
Prompt engineering and context engineering solve two different problems. Prompt engineering makes sure the model understands the task. Context engineering makes sure the model has what it needs to actually complete the task. You can see this most clearly by building one small agent and running it three ways: with neither improvement, with only prompt engineering, and with prompt engineering plus real context. That comparison is exactly what you are about to build.
Step 1: Install Ollama and Pull a Model
Ollama runs open-weight language models locally on your own machine. Install it for your platform from the official Ollama website’s instructions, or on Linux, run the official install script:
curl -fsSL https://ollama.com/install.sh | sh
This installs the Ollama service and starts it running in the background, listening on port 11434 by default. Verify it is running:
curl http://localhost:11434/api/tags
A working install returns a JSON list of any models you already have pulled (an empty list is fine on a fresh install). Next, pull a small, capable model. This tutorial uses Qwen3.5 at 4 billion parameters, a good balance of capability and speed on modest hardware:
ollama pull qwen3.5:4b
Ollama downloads the model in layers and verifies each one:
pulling manifest
pulling 81fb60c7daa8: 100% |##################| 3.4 GB
pulling 7339fa418c9a: 100% |##################| 11 KB
pulling 9371364b27a5: 100% |##################| 65 B
pulling de9fed2251b3: 100% |##################| 475 B
verifying sha256 digest
writing manifest
success
The bulk of that download, the 3.4 GB layer, is the model weights themselves. The smaller layers are the model’s configuration, template, and license metadata. Once you see success, the model is stored locally and ready to run with no further network access needed.
If your machine has less RAM available, substitute a smaller variant such as qwen3.5:0.8b everywhere this tutorial references qwen3.5:4b. The steps are identical either way.
Step 2: Set Up Your Python Environment
Create an isolated Python environment so this tutorial’s packages do not conflict with anything else on your system:
python3 -m venv venv
source venv/bin/activate
pip install langchain langchain-ollama
Confirm the installed versions:
pip show langchain langchain-ollama
Name: langchain
Version: 1.3.14
Name: langchain-ollama
Version: 1.1.0
This tutorial was built and verified against langchain 1.3.14 and langchain-ollama 1.1.0. LangChain v1 introduced a unified create_agent() function that this tutorial relies on, so make sure you have langchain>=1.0.0; running pip show langchain after the install above will confirm your installed version.
Step 3: Build and Run the Baseline Agent
Now build the smallest possible agent: a model with no system prompt, no tools, and no context beyond a single bare question. Create a file called agent.py:
from langchain.agents import create_agent
from langchain_ollama import ChatOllama
MODEL_NAME = "qwen3.5:4b"
def build_agent(system_prompt=None, num_ctx=4096, reasoning=None):
model = ChatOllama(model=MODEL_NAME, temperature=0, num_ctx=num_ctx, reasoning=reasoning)
return create_agent(model, tools=[], system_prompt=system_prompt)
if __name__ == "__main__":
baseline_agent = build_agent()
result = baseline_agent.invoke(
{"messages": [{"role": "user", "content": "Why did my service fail?"}]}
)
print(result["messages"][-1].content)
A few things worth understanding before you run this:
create_agent()is LangChain v1’s function for assembling a ready-to-run agent from a model, a list of tools, and an optional system prompt. Passing an emptytools=[]list means this agent can only respond with text; it cannot call any function or look anything uptemperature=0makes the model’s output as deterministic as possible, which is what you want when comparing outputs across runs. Higher temperatures add randomness that would make it harder to tell whether a difference in output came from your prompt or just from chancenum_ctxcontrols how many tokens of context Ollama allocates for this conversation, andreasoningcontrols whether the model shows hidden step-by-step thinking before its final answer. Both are left at their defaults for now; you will come back to why they matter in Step 5
Run it:
python3 agent.py
Here is real, unedited output from that exact script:
To help you troubleshoot why your service failed, I need a bit more context! Could you clarify:
1. Which specific service are you using? (e.g., cloud provider, app, API, website)
2. What error message or behavior did you see when it failed?
3. When and where does the failure occur?Common causes include:
– Network/connectivity issues
– Authentication/permission errors (e.g., expired tokens, wrong API keys)
– Configuration mismatches or outdated software versions
– Server-side outages/maintenance windows
– Insufficient resources (storage, bandwidth, etc.)Share more details so I can give targeted advice!
This is not a bad answer. It is a reasonable response to a genuinely vague question. But it is useless if you already have a specific failing service in front of you: it does not know what “my service” means, has no way to guess, and burns a full round trip just asking you to repeat information you have not given it yet. This is the baseline you will improve on.
Step 4: Add Prompt Engineering
Now improve the prompt without giving the agent any more information about the actual failure. Add a system prompt that assigns a role, anchors the output format, and explicitly permits the model to admit uncertainty:
PROMPT_ENGINEERED_SYSTEM = (
"You are a senior Linux systems administrator with deep expertise "
"in systemd and production incident response. When a user describes "
"a service failure, respond in exactly this format:\n"
"Root Cause: \n"
"Fix: \n"
"Verification Command: \n"
"If you do not have enough information to identify the root cause, "
"say so explicitly instead of guessing."
)
prompt_agent = build_agent(system_prompt=PROMPT_ENGINEERED_SYSTEM)
result = prompt_agent.invoke({
"messages": [{
"role": "user",
"content": "My systemd service demo-broken-app.service failed to start. Why?",
}]
})
print(result["messages"][-1].content)
Notice what changed and what did not. The question is now specific (it names a real service), and the system prompt gives the model a role, a required format, and explicit permission to say “I don’t know” instead of inventing an answer. What still has not changed is the information available: the model has no logs, no error message, nothing beyond a service name. Real output from this exact code:
Root Cause: Insufficient context is provided to determine the specific failure reason without access to service logs or configuration details.
Fix: Retrieve detailed diagnostics usingjournalctl -u demo-broken-app.service, inspect unit files for syntax errors withsystemd-analyze verify, and check system resources viatop.
Verification Command:journalctl -xe --unit=demo-broken-app.service
This is a meaningfully better answer than the baseline, and it is worth being precise about why. The model followed the format exactly, gave concrete and correct next steps for someone with no logs in hand yet, and, notably, did not hallucinate a specific cause it could not actually know. That last part is the real payoff of prompt engineering here: a well-engineered prompt does not make the model psychic, but it does make an honest “I don’t know, here’s how to find out” far more likely than a confident-sounding guess. What it still cannot do is name the actual root cause, because that information has never been given to it. That is context engineering’s job, not prompt engineering’s.
Step 5: Add Context Engineering
To demonstrate context engineering properly, you need a real failure to diagnose, not a hypothetical one. Create a small Python script that intentionally crashes because it is missing a required environment variable, a common and realistic misconfiguration:
import os
def main():
print("Starting invoice-worker...", flush=True)
database_url = os.environ["DATABASE_URL"]
print(f"Connecting to {database_url}", flush=True)
if __name__ == "__main__":
main()
Save this as /opt/demo-broken-app/app.py. Now wire it up as a systemd service, deliberately without setting DATABASE_URL:
sudo mkdir -p /opt/demo-broken-app
sudo tee /etc/systemd/system/demo-broken-app.service > /dev/null << 'EOF'
[Unit]
Description=Invoice worker demo app (intentionally broken for tutorial)
[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/demo-broken-app/app.py
Restart=no
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl start demo-broken-app.service
systemctl daemon-reload tells systemd to re-read unit files from disk before you try to use a new or changed one; skipping it is a common source of “why isn’t my change taking effect” confusion. Now check what happened:
systemctl status demo-broken-app.service --no-pager -l
Real output from this exact sequence:
× demo-broken-app.service - Invoice worker demo app (intentionally broken for tutorial)
Loaded: loaded (/etc/systemd/system/demo-broken-app.service; disabled; preset: enabled)
Active: failed (Result: exit-code) since Sun 2026-07-26 14:48:17 EDT; 5s ago
Duration: 424ms
Process: 533507 ExecStart=/usr/bin/python3 /opt/demo-broken-app/app.py (code=exited, status=1/FAILURE)
Main PID: 533507 (code=exited, status=1/FAILURE)
That tells you the service failed, but not why. For the actual reason, pull the service’s journal:
journalctl -u demo-broken-app.service --no-pager -n 50
Jul 26 14:48:16 vixn systemd[1]: Started demo-broken-app.service - Invoice worker demo app (intentionally broken for tutorial).
Jul 26 14:48:16 vixn python3[533507]: Starting invoice-worker...
Jul 26 14:48:17 vixn python3[533507]: Traceback (most recent call last):
Jul 26 14:48:17 vixn python3[533507]: File "/opt/demo-broken-app/app.py", line 10, in <module>
Jul 26 14:48:17 vixn python3[533507]: main()
Jul 26 14:48:17 vixn python3[533507]: File "/opt/demo-broken-app/app.py", line 6, in main
Jul 26 14:48:17 vixn python3[533507]: database_url = os.environ["DATABASE_URL"]
Jul 26 14:48:17 vixn python3[533507]: ~~~~~~~~~~^^^^^^^^^^^^^^^^
Jul 26 14:48:17 vixn python3[533507]: KeyError: 'DATABASE_URL'
Jul 26 14:48:17 vixn systemd[1]: demo-broken-app.service: Main process exited, code=exited, status=1/FAILURE
Jul 26 14:48:17 vixn systemd[1]: demo-broken-app.service: Failed with result 'exit-code'.
Now you have real context: an actual Python traceback showing exactly which environment variable is missing. Feed both of these real outputs into the same prompt-engineered agent from Step 4, changed only by adding this data to the user message:
context_message = f"""My systemd service demo-broken-app.service failed to start. Why?
Here is the output of `systemctl status demo-broken-app.service`:
{status_output}
Here is the output of `journalctl -u demo-broken-app.service --no-pager -n 50`:
{journalctl_output}"""
context_agent = build_agent(system_prompt=PROMPT_ENGINEERED_SYSTEM)
result = context_agent.invoke({"messages": [{"role": "user", "content": context_message}]})
print(result["messages"][-1].content)
Here is real, unedited output from the first attempt at running that exact code:
Root Cause: The application crashed because the required environment variable
DATABASE_URLwas missing from the process context when Python executed under systemd.
Fix: Add an[Environment]section to/etc/systemd/system/demo-broken-app.servicecontainingDATABASE_URL=your_connection_string, then runsystemctl daemon-reload && systemctl restart demo-brown-app.service. (Wait, typo in thought again). Correct name is broken.Let’s fix the typos and ensure strict adherence.
Fix: Add an[Environment]section […] then runsystemctl daemon-reload && systemctl restart demo-brown-app.service. (Wait, I keep typing brown instead of broken). Okay, stop that mental slip.[…continues second-guessing itself over “broken” versus “brown” for several more paragraphs without ever completing a clean final answer…]
Look closely at the first sentence, and this is worth sitting with for a second: the root cause is exactly correct, identified immediately, in the first sentence, with the specific missing variable named. Context engineering did its job. But the response then falls apart into a rambling, self-correcting loop about whether the service is named “broken” or “brown”, something neither the logs nor the question ever actually said. This is a real gotcha, not a fabricated one, and it is worth understanding rather than just retrying until it goes away.
Diagnosing the Gotcha: Context Window Size
The model itself was never confused about the file name; every quoted command above correctly says demo-broken-app.service. What happened is that the response got cut off mid-generation and looped back on itself, which is a symptom of running out of room, not a reasoning failure. Check how much context Ollama actually allocated for this model:
curl -s http://localhost:11434/api/show -d '{"model":"qwen3.5:4b"}' | python3 -c "
import json, sys
d = json.load(sys.stdin)
print('max supported:', d['model_info']['qwen35.context_length'])
"
max supported: 262144
The model architecture supports up to 262,144 tokens of context, but that is not what actually gets allocated. The num_ctx parameter on ChatOllama defaults to None, meaning it does not request a specific context size at all; when that happens, Ollama’s server picks a default based on available VRAM. Per Ollama’s own documentation, machines with less than 24 GiB of VRAM default to a 4k (4096 token) context window, and a CPU-only machine with no dedicated GPU, like the one this tutorial was built on, falls into that same bucket. You can confirm exactly what a running model is using with ollama ps, which shows a CONTEXT column.
Qwen3.5 is also a “thinking” model: by default it spends tokens on hidden reasoning before it writes the visible answer you actually see. Add a system prompt, a full journalctl dump, and a systemctl status block to that hidden reasoning process, and a 4096-token window can fill up before the model ever finishes writing its final answer, which is exactly what produces this kind of derailed, looping output.
There are two independent ways to fix this. You can give the reasoning more room by raising num_ctx (8192 or higher). Or, since this task only needs a short, formatted final answer and does not benefit from visible step-by-step reasoning, you can turn thinking off entirely with Ollama’s documented think parameter, exposed in ChatOllama as reasoning:
context_agent = build_agent(
system_prompt=PROMPT_ENGINEERED_SYSTEM,
num_ctx=4096,
reasoning=False,
)
This is the cleaner fix for a task like this one: it removes the hidden reasoning tokens that were consuming the context budget in the first place, rather than just giving them more room to expand into. Re-running the exact same prompt and the exact same context, with only that change, produces a fast, clean, complete answer:
Root Cause: The application crashed because the required environment variable
DATABASE_URLwas not defined in its execution context.
Fix: Export or set theDATABASE_URLenvironment variable before running the service (e.g., via.env, systemd drop-in, or manual export) and reload/restart the unit withsystemctl daemon-reload && systemctl restart demo-broken-app.service.
Verification Command:journalctl -u demo-broken-app.service --no-pager | grep "Starting invoice-worker"
This is the complete picture. Prompt engineering got the model to follow a format and admit uncertainty honestly instead of guessing. Context engineering, in the form of real log data, got the model to the correct root cause immediately, in every single run, regardless of whether the final formatting came out clean. And a third, easy-to-miss detail, how much of a small local model’s limited context window is quietly consumed by hidden reasoning, determined whether that correct answer arrived as a clean final response or got lost in a loop. All three matter, and none of them are visible from the outside until you go looking.
Step 6: Verify the Diagnosis Was Actually Correct
An AI agent’s suggested fix is only a hypothesis until you have actually tested it. Never apply a suggested fix to a real system without verifying it worked. Apply the fix the agent recommended, setting DATABASE_URL in the unit file:
sudo tee /etc/systemd/system/demo-broken-app.service > /dev/null << 'EOF'
[Unit]
Description=Invoice worker demo app (intentionally broken for tutorial)
[Service]
Type=simple
Environment=DATABASE_URL=postgresql://invoice_worker@localhost:5432/invoices
ExecStart=/usr/bin/python3 /opt/demo-broken-app/app.py
Restart=no
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl start demo-broken-app.service
The Environment= directive is the standard systemd way to pass an environment variable to a service’s process; it is documented in the official systemd.exec manual page. Check the result:
systemctl status demo-broken-app.service --no-pager -l
○ demo-broken-app.service - Invoice worker demo app (intentionally broken for tutorial)
Loaded: loaded (/etc/systemd/system/demo-broken-app.service; disabled; preset: enabled)
Active: inactive (dead)
journalctl -u demo-broken-app.service --no-pager -n 5
Jul 26 14:48:32 vixn systemd[1]: Started demo-broken-app.service - Invoice worker demo app (intentionally broken for tutorial).
Jul 26 14:48:32 vixn python3[533661]: Starting invoice-worker...
Jul 26 14:48:32 vixn python3[533661]: Connecting to postgresql://invoice_worker@localhost:5432/invoices
Jul 26 14:48:32 vixn systemd[1]: demo-broken-app.service: Deactivated successfully.
The service now starts, connects using the environment variable you supplied, and exits cleanly (this demo script does not loop or daemonize, so “inactive (dead)” after a successful run is expected, not a new failure). The diagnosis was correct. Clean up the demo service when you are done experimenting:
sudo systemctl stop demo-broken-app.service
sudo systemctl disable demo-broken-app.service
sudo rm -f /etc/systemd/system/demo-broken-app.service
sudo systemctl daemon-reload
sudo systemctl reset-failed
sudo rm -rf /opt/demo-broken-app
Common Mistakes and Gotchas
Treating a Silent Truncation as a Reasoning Failure
As Step 5 demonstrated directly, a response that trails off, repeats itself, or loops is often a context window problem, not evidence that the model “can’t reason.” Before concluding a model is unreliable, check the actual allocated context with ollama ps, compare it against how much text you are sending (system prompt, injected context, and hidden reasoning for “thinking” models all count), and either raise num_ctx or turn thinking off for tasks that only need a short final answer.
Confusing the Two Techniques
It is tempting to treat “write a better prompt” and “give the model more information” as the same lever turned up or down. They are not. A beautifully worded prompt cannot substitute for missing facts, and dumping in more raw data will not fix a vague, unstructured question. Diagnose which one you are actually short on before reaching for either.
Context Pollution
More context is not automatically better context. Pasting an entire log file, an entire codebase, or an entire ticket history at a model can bury the one relevant line under thousands of irrelevant ones, and every extra token also eats into the context budget described above. Include what is relevant to the task, not everything you have.
Trusting a Confident-Sounding Answer Without Verification
Step 6 was not optional filler. An AI agent can state a wrong root cause with exactly the same confident tone as a correct one. Always verify a suggested fix against the real system, the same way you would verify a suggestion from a colleague.
How to Verify It All Worked
You have completed this tutorial successfully if:
ollama listshowsqwen3.5:4b(or your chosen model) installed locally- The baseline agent (Step 3) produced a vague, clarifying-question style answer
- The prompt-engineered agent (Step 4) produced a correctly formatted answer that honestly admitted it lacked enough information, rather than guessing
- The context-engineered agent (Step 5) named
DATABASE_URLas the specific missing variable in every run, and produced a clean, complete, correctly formatted answer once thinking was disabled (or given a larger context window) - Applying the suggested fix (Step 6) made the demo service start and exit cleanly instead of failing
If your context-engineered output still looks truncated or repetitive after setting reasoning=False, double check you are running langchain-ollama>=1.1.0 so the parameter is actually supported, or fall back to raising num_ctx further (16384 is a safe next step for most local hardware).
Next Steps
From here, a few natural directions to keep building on what you have learned:
- Give the agent an actual tool, for example a Python function that runs
journalctlitself, instead of manually pasting log output into the prompt. That turns manual context engineering into automatic context retrieval - Experiment with few-shot prompting: add one or two example Root Cause and Fix pairs to the system prompt and see how much more consistent the output format becomes
- Try the same three-way comparison (baseline, prompt-engineered, context-engineered) on a task from your own work, using your own real logs or documentation as the context
- If you want to see context engineering applied to a fuller agent with real tool calls, see this site’s tutorials on building an AI agent with function calling in Node.js and Ollama and building a self-correcting AI agent with LangGraph and Ollama








No Comment! Be the first one.