Repository navigation
Test and tune the planner's web search and fetch use - #712
Conversation
|
Thank you for the great experiments and analysis @IZO-Ong! I appreciate the detailed work behind the final seemingly simple prompt change. The experiment structure, where each stage carries the winning approach forward, is efficient, a good fit here and makes the results easy to follow. The experiments are carefully designed. E.g. calibrating where facts sit in each page, then requiring them in both the answer and a fetched page, is a neat way to test truncation and to tell reading from remembering. It looks tricky to fully optimise this, especially within the constraints of the narrow allowlist. The final minimal change is well supported and a great solution. There are a couple of things we could tweak before merging:
A few other questions I would like to hear your views on, not requests for changes:
|
|
Hi Hanna! Sorry for the late reply. I agree with both changes. I'll take out the runner, variants, calibration and winner logic, and add per-call web details to the planner's meta. The full experiment is at b0b8f8f if you want to tag it. First-turn refusals: I don't think they're a big concern now, as a refused fetch only returns an error code, usually within the same API response as the search that follows. As such, refused fetches only cost a few seconds and few tokens rather than a full planner round. Moreover, the fix I can think likely will cost more than they save. We can try injecting a URL index of every allowed site adds tokens to every turn, but injecting just the entry pages (expt 1b) added ~26k input tokens per run and didn't reduce refusals. Wasted searches: I saw one in Stage 0, where one of the three off_allowlist runs made a single search, probably when it tried to access hl7.org / docs.openfn.org for DHIS2 tracker docs. I can't see what it found, as the trace only kept "ok" for searches. For this, I'll record search result URLs in the new meta in order to count searches whose results were never opened as well. Additionally, this off_allowlist wasn't rerun with the new prompt, which might search more often. Allowlist vs open web: I would keep the allowlist as long as the planner reads web pages in the same context where it edits workflows and job code. Mainly, this boils down to the allowlist being quite effective, with 0 attempts at banned URLs, and for domains off the list, the planner answers from memory and says it couldn't check the source. To make web search more useful, we would probably need more domains, and we should test each one to check that the web search tools can actually read it. One thing we could do is tell the model in the prompt what each domain covers might stop it searching sites for things they don't have. In the long run, we could give the web tools to a read-only research subagent with no workflow-editing tools that returns what it found and its sources. That way, the planner would never see the pages and injected text would have to survive the summary, thus preventing the subagent from acting on it. |
|
Thanks @IZO-Ong, this all makes sense, and the trimmed PR and the new Langfuse logging look good. I’ll tag b0b8f8f and link it from #694 so the full experiment stays reproducible. Makes sense on first-turn refusals, it isn’t worth spending more turn tokens to remove it, especially since injecting entry URLs didn’t help. Recording search result URLs is a nice touch, and will help us count searches that didn’t lead anywhere. Your allowlist reasoning make sense too, and I love the read-only research subagent idea – I think we'll be implementing that! Keeping fetched pages out of the context that edits workflows is a much cleaner security boundary than any list of domains. Thanks again for such careful, well-structured work. It’s been great to review. |
hanna-paasivirta
left a comment
There was a problem hiding this comment.
Looks perfect. Thank you!
Short Description
Adds a test layer that records how the global chat planner uses
web_search/web_fetch, uses it to run a staged experiment on the planner's prompt and config, and ships a prompt line telling the planner to search before fetching.Fixes #694
Implementation Details
What ships to production
One bullet appended to
planner_web_tools_promptinservices/global_chat/prompts.yaml:max_content_tokens,max_uses, the allowlist and planner code are unchanged.Test layer (
services/global_chat/tests/web_tools/)The questions in #694 are about how the tools are used (order, URLs, refusals, re-fetches). As such, the checks assert on a recorded tool-call trace:
trace.py: turns the planner's raw API responses into an ordered list of web calls, pairing calls to results bytool_use_idacross rounds.recording.py:RecordingPlannerkeeps each raw response, and variants are applied to config/prompts in memory.metrics.py: per-run metrics and the stage winner rule.scenarios.py: six scenarios, each with a trap (two controls that should never touch the web, a shallow and a deep FHIR question, an off-allowlist DHIS2 question, a 3-turn conversation).run_web_experiments.py: the staged runner. Runs are cached on disk keyed by a hash of the resolved model, planner config, prompts and scenario.calibrate.py: fetched the FHIR pages at 10k/25k/50k tokens to find where facts land, which is how the scenario facts were chosen.Experiment results
The test suite does N=3 runs per cell against
claude-opus-4-8. "Grounded" means the fact is in the answer and in a fetched page, suggesting it was read and not recalled.Stage 0 (baseline): every FHIR first turn (9/9) opened by fetching
patient.htmlfrom memory and being refused (url_not_in_prior_context).Stage 0 table
Stage 1 (fetch strategy): in almost every run, the model's first move was to fetch a URL it remembered (patient.html), which web_fetch refuses. With the search-first line (1a), the model searched, fetched the right page and answered from it every time (6/6), against 3/6 for base.
Stage 1 table (FHIR scenarios)
Stage 2 (
max_content_tokens, carrying 1a): keep 10k.fhir_deepnever fetched the long Patient page at any size (0/9). The model is smart enough to find the short value-set page, so the model routes around truncation when a short page has the answer.Stage 2 table
Stage 3 (findings across turns, carrying 1a): with 1a, no follow-up turn re-fetched, and 2/3 multi-turn runs opened with no refused fetch at all. The findings line only added costs.
Stage 3 table
Answers to #694:
url_not_in_prior_contextmax_content_tokensTest results on the shipped prompt
Deviations from the design
The strict suite allows up to 2 refused fetches on a first turn, where the design asked for 0. The model nearly always opens a FHIR question by trying a URL it remembers, which gets refused. No prompt change was able to stop this, although the new shipped line saw a smaller amount of 1–2 refusals per run, while the old prompt had runs with 4. In follow-up turns, any refused fetch still fails.
Follow-ups
response/history, or separate rounds with a delimiter.Steps to run test suites
The live suites need an
ANTHROPIC_API_KEYforapi.anthropic.com.AI Usage
Please disclose whether you've used AI in this work (it's cool, we just want to
know!):
You can read more details in our
Responsible AI Policy