Skip to content

Commit ff2f885

Browse files
fix: wire real retrieval into optimized prompts + make Part 3 a genuine tool-calling agent
Final review found the optimizer never saw the (fictional) docs, so the exact-match RAG lesson could not work, and Part 3's agent was dead code. Part 1-2: attach retrieved context (rag_app.retrieve) to each eval row and inject it via the ChatPrompt user template (retrieve-then-generate); retrieval is fixed, the prompt is optimized. Print baseline vs optimized .system so the refinement is visible. Verified offline: 18/18 exact answers appear in the retrieved top-3 context. Part 3: replace the never-optimized agentic_answer gate with a genuine tool-calling agent (search_docs -> rag_app.retrieve, allow_tool_use) whose system prompt is actually optimized; note optimize_tools=True as the one-line tool-description add; FewShot tunes the same agent. Tool binding verified against the optimizer's own resolve_toolcalling_tools. README Part 3 + the Part 4 selection table updated to match.
1 parent ecd3062 commit ff2f885

2 files changed

Lines changed: 85 additions & 25 deletions

File tree

guides/prompt_agent_optimization/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ step is a comparable run.
1919
- **Part 1** ⭐ — your first optimization: exact-match metric + `MetaPromptOptimizer`.
2020
- **Part 2** — LLM-judge metrics, *how to trust a judge*, and multi-objective
2121
optimization with `MultiMetricObjective`.
22-
- **Part 3** — from prompt to agent: a retrieval gate, `FewShotBayesianOptimizer`,
23-
and `ParameterOptimizer`.
22+
- **Part 3** — from prompt to agent: a tool-calling `search_docs` agent optimized
23+
end-to-end, then `FewShotBayesianOptimizer` on the same agent (with a pointer to
24+
`ParameterOptimizer`).
2425
- **Part 4** — choosing an optimizer (selection table + how to choose + chaining).
2526
- **Part 5** — promote the winner to the Prompt Library; pointers to Optimization
2627
Studio and the docs.

guides/prompt_agent_optimization/prompt_agent_optimization.ipynb

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,17 @@
8585
"outputs": [],
8686
"source": [
8787
"exact_cases = data.load_exact_cases()\n",
88+
"\n",
89+
"# RAG step — retrieve. For each question, pull the most relevant docs with our\n",
90+
"# real ChromaDB retriever and attach them as `context`. A production RAG system\n",
91+
"# retrieves per query at answer time; we do it once here so every optimizer trial\n",
92+
"# answers the SAME question from the SAME context. What we optimize is the\n",
93+
"# *prompt*, not the retriever.\n",
94+
"for case in exact_cases:\n",
95+
" case[\"context\"] = \"\\n\\n\".join(rag_app.retrieve(case[\"query\"]))\n",
96+
"\n",
8897
"exact_dataset = data.build_dataset(client, \"ledgerline-exact\", exact_cases)\n",
89-
"print(f\"Dataset 'ledgerline-exact' has {len(exact_cases)} cases\")"
98+
"print(f\"Dataset 'ledgerline-exact' has {len(exact_cases)} cases (each with retrieved context)\")"
9099
]
91100
},
92101
{
@@ -128,7 +137,9 @@
128137
"source": [
129138
"#### The starting prompt\n",
130139
"\n",
131-
"Here is our baseline system prompt — deliberately mediocre, so there's room to improve. This is the `ChatPrompt` the optimizer will rewrite. `{query}` is filled from each dataset row."
140+
"Here is our baseline system prompt — deliberately mediocre, so there's room to improve. This is the `ChatPrompt` the optimizer will rewrite.\n",
141+
"\n",
142+
"Look at the **user template**: `{context}` is filled with the docs we just retrieved and `{query}` with the question — both come from each dataset row. That's the **retrieve-then-generate** shape of a real RAG system. Retrieval is held fixed; what we optimize is how the **system prompt** tells the model to turn that context into a correct, concise answer."
132143
]
133144
},
134145
{
@@ -145,7 +156,7 @@
145156
"prompt = ChatPrompt(\n",
146157
" name=\"ledgerline-answer\",\n",
147158
" system=BASELINE_SYSTEM,\n",
148-
" user=\"{query}\",\n",
159+
" user=\"Context:\\n{context}\\n\\nQuestion: {query}\",\n",
149160
" model=config.GEN_MODEL,\n",
150161
")"
151162
]
@@ -188,7 +199,13 @@
188199
"\n",
189200
"print(\"Baseline score:\", result.initial_score)\n",
190201
"print(\"Best score: \", result.score)\n",
191-
"print(\"\\nOptimized system prompt:\\n\", result.prompt)"
202+
"\n",
203+
"# See HOW the prompt was refined: the optimizer rewrote the *system* instructions.\n",
204+
"# result.prompt is a ChatPrompt; .system is the optimized system text.\n",
205+
"print(\"\\n--- Baseline system prompt ---\")\n",
206+
"print(BASELINE_SYSTEM)\n",
207+
"print(\"\\n--- Optimized system prompt ---\")\n",
208+
"print(result.prompt.system)"
192209
]
193210
},
194211
{
@@ -198,7 +215,7 @@
198215
"source": [
199216
"#### See it in Opik\n",
200217
"\n",
201-
"Open **Evaluation → Optimization runs** in your Opik workspace. You'll see this run with every candidate prompt, its score, and the trace for each trial. Compare the baseline row to the best row — that delta is your improvement.\n",
218+
"The cell above printed the **baseline vs optimized system prompt** side by side — that rewrite is the concrete refinement the optimizer found. Now open **Evaluation → Optimization runs** in your Opik workspace: you'll see this run with every candidate prompt, its score, and the trace for each trial. Compare the baseline row to the best row — that delta is your improvement.\n",
202219
"\n",
203220
"🎓 **This is where the live workshop ends.** You've run a real optimization and improved a prompt, measured against a dataset, stored in Opik. Everything below builds on exactly this loop."
204221
]
@@ -289,12 +306,14 @@
289306
"outputs": [],
290307
"source": [
291308
"judge_cases = data.load_judge_cases()\n",
309+
"for case in judge_cases:\n",
310+
" case[\"context\"] = \"\\n\\n\".join(rag_app.retrieve(case[\"query\"]))\n",
292311
"judge_dataset = data.build_dataset(client, \"ledgerline-judge\", judge_cases)\n",
293312
"\n",
294313
"judge_prompt = ChatPrompt(\n",
295314
" name=\"ledgerline-answer-judge\",\n",
296315
" system=BASELINE_SYSTEM,\n",
297-
" user=\"{query}\",\n",
316+
" user=\"Context:\\n{context}\\n\\nQuestion: {query}\",\n",
298317
" model=config.GEN_MODEL,\n",
299318
")\n",
300319
"\n",
@@ -352,7 +371,7 @@
352371
" n_samples=8,\n",
353372
")\n",
354373
"print(\"Multi-objective best score:\", multi_result.score)\n",
355-
"print(\"\\nOptimized prompt:\\n\", multi_result.prompt)"
374+
"print(\"\\nOptimized system prompt:\\n\", multi_result.prompt.system)"
356375
]
357376
},
358377
{
@@ -372,7 +391,11 @@
372391
"source": [
373392
"## Part 3 — From prompt to agent\n",
374393
"\n",
375-
"So far we optimized a single answer prompt. Real systems are agents: they *decide* what to do. Our RAG app can grow a **retrieval gate** — decide whether a question even needs a docs lookup (cheap questions skip retrieval). That decision is itself a prompt, and the same optimizer loop tunes it."
394+
"So far retrieval was **fixed**: we retrieved once, put the docs in the prompt, and optimized the wording. Real systems are agents — they *decide* what to do. Here we hand the model a **`search_docs` tool** wired to our retriever and let it choose when to call it. That turns the prompt into an **agent**, and the same optimizer loop tunes it.\n",
395+
"\n",
396+
"**What \"optimizing an agent\" means:** not rewriting the tool's code — the retriever is fixed. It means optimizing the natural-language surface the agent reasons over: its **system prompt** (when to search, how to answer from results) and, optionally, its **tool descriptions** (`optimize_tools=True`) so it calls the tool at the right moments.\n",
397+
"\n",
398+
"*(A lighter alternative to a tool is a yes/no retrieval gate — see `rag_app.should_retrieve` — optimized with the same loop. We use a real tool here because it better reflects a production agent.)*"
376399
]
377400
},
378401
{
@@ -382,18 +405,54 @@
382405
"metadata": {},
383406
"outputs": [],
384407
"source": [
385-
"@opik.track\n",
386-
"def agentic_answer(query: str, system_prompt: str) -> str:\n",
387-
" # Agent step: decide whether to retrieve, then answer accordingly.\n",
388-
" if rag_app.should_retrieve(query):\n",
389-
" return rag_app.answer(query, system_prompt=system_prompt)\n",
390-
" messages = [\n",
391-
" {\"role\": \"system\", \"content\": system_prompt},\n",
392-
" {\"role\": \"user\", \"content\": query},\n",
393-
" ]\n",
394-
" import litellm\n",
395-
" resp = litellm.completion(model=config.GEN_MODEL, messages=messages)\n",
396-
" return resp.choices[0].message.content"
408+
"# The tool the agent may call. It wraps our real ChromaDB retriever; the *agent*\n",
409+
"# decides when to call it. Returning one string keeps the tool result clean.\n",
410+
"def search_docs(query: str) -> str:\n",
411+
" \"\"\"Search the Ledgerline documentation and return the most relevant snippets.\"\"\"\n",
412+
" return \"\\n\\n\".join(rag_app.retrieve(query))\n",
413+
"\n",
414+
"\n",
415+
"SEARCH_DOCS_TOOL = {\n",
416+
" \"type\": \"function\",\n",
417+
" \"function\": {\n",
418+
" \"name\": \"search_docs\",\n",
419+
" \"description\": \"Search the Ledgerline product documentation for relevant snippets.\",\n",
420+
" \"parameters\": {\n",
421+
" \"type\": \"object\",\n",
422+
" \"properties\": {\n",
423+
" \"query\": {\"type\": \"string\", \"description\": \"What to look up in the docs.\"},\n",
424+
" },\n",
425+
" \"required\": [\"query\"],\n",
426+
" },\n",
427+
" },\n",
428+
"}\n",
429+
"\n",
430+
"AGENT_SYSTEM = \"You are a Ledgerline support agent. Use tools when they help.\"\n",
431+
"\n",
432+
"# tools + function_map make this ChatPrompt an agent: on a tool call, the\n",
433+
"# optimizer executes search_docs and feeds the result back to the model.\n",
434+
"agent_prompt = ChatPrompt(\n",
435+
" name=\"ledgerline-agent\",\n",
436+
" system=AGENT_SYSTEM,\n",
437+
" user=\"{query}\",\n",
438+
" tools=[SEARCH_DOCS_TOOL],\n",
439+
" function_map={\"search_docs\": search_docs},\n",
440+
" model=config.GEN_MODEL,\n",
441+
")\n",
442+
"\n",
443+
"# optimize_prompts defaults to \"system\": we tune the agent's instructions.\n",
444+
"# (Flip optimize_tools=True to ALSO let the optimizer refine the tool description.)\n",
445+
"agent_result = optimizer.optimize_prompt(\n",
446+
" prompt=agent_prompt,\n",
447+
" dataset=judge_dataset,\n",
448+
" metric=answer_relevance,\n",
449+
" max_trials=8,\n",
450+
" n_samples=8,\n",
451+
" allow_tool_use=True,\n",
452+
")\n",
453+
"print(\"Agent baseline:\", agent_result.initial_score, \"-> best:\", agent_result.score)\n",
454+
"print(\"\\n--- Optimized agent system prompt ---\")\n",
455+
"print(agent_result.prompt.system)"
397456
]
398457
},
399458
{
@@ -403,7 +462,7 @@
403462
"source": [
404463
"#### When demonstrations matter: Few-Shot Bayesian\n",
405464
"\n",
406-
"If the win comes from *showing examples* rather than rewording instructions, reach for `FewShotBayesianOptimizer` — it uses Bayesian search (Optuna) to pick the best set and order of few-shot demonstrations to attach to your prompt. Same API as before."
465+
"If the win comes from *showing examples* rather than rewording instructions, reach for `FewShotBayesianOptimizer` — it uses Bayesian search (Optuna) to pick the best set and order of few-shot demonstrations to attach. We point it at the **same agent**, so it tunes the agent's examples rather than a fresh prompt."
407466
]
408467
},
409468
{
@@ -418,7 +477,7 @@
418477
"fewshot_optimizer = FewShotBayesianOptimizer(model=config.OPTIMIZER_MODEL, n_threads=4)\n",
419478
"\n",
420479
"fewshot_result = fewshot_optimizer.optimize_prompt(\n",
421-
" prompt=judge_prompt,\n",
480+
" prompt=agent_prompt,\n",
422481
" dataset=judge_dataset,\n",
423482
" metric=answer_relevance,\n",
424483
" n_samples=8,\n",
@@ -452,7 +511,7 @@
452511
"| **Few-Shot Bayesian** | Picking the best demonstrations | Part 3 |\n",
453512
"| **Evolutionary** | Exploring diverse structures; multi-objective | (see multi-objective, Part 2) |\n",
454513
"| **GEPA** | Single-turn, reflection-heavy tasks (`pip install gepa`) | (try on your own) |\n",
455-
"| **Parameter** | Temperature / top_p, prompt unchanged | Part 3 |\n",
514+
"| **Parameter** | Temperature / top_p, prompt unchanged | Part 3 (described) |\n",
456515
"\n",
457516
"**How to choose, in four questions:**\n",
458517
"1. **What's the constraint** — wording, examples, tool use, or sampling params?\n",

0 commit comments

Comments
 (0)