Trajectory Evaluation: It Reached the Destination, but How Many Things Did It Hit
Looking only at the outcome lets a whole class of failures through: the answer was right, but it cost ten times as much, took seven detours, and touched a tool it had no business touching. Today you score the trajectory itself with tool-sequence checks, loop detection and step budgets, then extend single-turn evaluation to multi-turn with a simulated user.
Today's Goals
- Name three failure classes only a trajectory reveals, and give an automatically checkable rule for each
- Implement loop detection and a step budget, and explain why the threshold belongs to the task rather than to the whole suite
- Run a multi-turn evaluation with a simulated user, and say what bias the simulated user itself introduces
Plain-Language Walkthrough
A dashcam
A car arrives at its destination on time. What does that tell you?
It tells you the car arrived. Whether it ran a red light, circled the same intersection four times, or cut down a one-way street the wrong way — none of that is readable from the fact of arrival.
That is exactly why a dashcam exists. It does not replace the question of whether you got there; arrival is still the first question. What it adds is what happened along the way. After a collision, the insurer asks for the footage, not the itinerary.
The first three days built evaluations that are all itinerary: was the outcome right, how did the judge score it. Today you install the dashcam.
What outcome-only grading lets through
Start with real numbers. This is the first task in today's lab, run twenty times:
ev-101-refund-in-window [positive]
passed 13/20 mean 2.6 steps max 3 steps
failure breakdown: sequence 7Notice the part that matters: the outcome was correct all twenty times. Twenty refunds, not one too many or too few, every order id right, every amount right. If all you have is the outcome grader from Day 1, this task reports 20 out of 20 and looks flawless.
What actually happened is that on seven of those twenty runs, the agent issued the refund without ever checking the refund policy. Those seven still came out right purely because this task's order happened to sit inside the refund window. It guessed correctly. Hand it an expired order and the identical behavior is money refunded that should not have been.
This class of failure shares one property: it is invisible in the outcome, and it is the precursor to the next incident. An outcome-only suite stays green right up until the day it goes red, and by then you have already paid out.
Only three failure classes are worth reading the trajectory for. Everything else, leave alone:
| Failure | What it looks like | Automatically checkable rule |
|---|---|---|
| Skipped a required call | Jumped past a lookup or a validation step | Before the first call to tool X, tool Y must have appeared |
| Called something forbidden | Touched a tool this task explicitly rules out | Every tool on the deny list has a call count of zero |
| Detours and loops | Same query repeated, step count or spend over budget | Caps on repeats of one call signature, total steps, and total spend |
Why "called something forbidden" is the dangerous one
The second class deserves its own section.
A skipped prerequisite is invisible today, but it at least has a tendency to get caught by the outcome eventually: an agent that skips the policy check will sooner or later refund something it should not have. "Called something forbidden," by contrast, very often leaves the outcome completely correct.
Today's lab has a task built to demonstrate exactly this. The user says:
For order C3003 I just want to confirm whether it was already refunded. Please don't process anything.That is a pure lookup. The correct behavior is to check, reply "it was already refunded," and stop. The target agent sees an already-refunded order and, helpfully, opens a human escalation ticket.
Across twenty runs: the outcome grader passes all twenty, and the tool-sequence grader fails all twenty.
Why can't the outcome see it? Because the correct outcome for this task is "nothing changed" — no new refund record. And the outcome after the stray escalation ticket is also "no new refund record." The two are identical at the outcome layer. You could add an assertion saying no escalation should exist, but that patches exactly one tool. A real system also sends email, sends SMS, notifies third parties, writes audit logs. You cannot pre-write a "this must not happen" assertion for every tool that exists.
A deny list is not a nice-to-have. It covers the region an outcome grader is structurally unable to reach.
How strict a sequence check should be
Day 1 laid down a principle: check what it produced, not which steps it took. Today is not a reversal, it is where that principle gets its boundary drawn.
The easiest mistake is to write the grader like this: I know the right flow is look up the order, check the policy, issue the refund, so I will assert the tool-call sequence equals those three names position by position.
That assertion misfires in two ways. First, the agent looks up the order a second time to confirm the amount — the sequence is now four long, the grader fails it, and it was being more careful than required. Second, the agent works out that two lookups can be issued concurrently — the order flips, the grader fails it, and it got faster. Your grader is punishing improvement.
The right granularity is to check only the prerequisites that genuinely matter, and to check them as a partial order rather than a total one:
// One question only: before the first `after` call, did `before` ever appear?
export function checkSequence(toolCalls, policy) {
const violations = []
for (const rule of policy.requireBefore ?? []) {
const afterAt = toolCalls.findIndex((c) => c.name === rule.after)
// `after` was never called, so the rule has nothing to say. Not a violation.
if (afterAt === -1) continue
const satisfied = toolCalls.slice(0, afterAt).some((c) => c.name === rule.before)
if (!satisfied) {
violations.push({ kind: 'missing-prerequisite', tool: rule.before })
}
}
return violations
}# One question only: before the first `after` call, did `before` ever appear?
def check_sequence(tool_calls, policy):
violations = []
for rule in policy.get("requireBefore", []):
after_at = next(
(i for i, c in enumerate(tool_calls) if c["name"] == rule["after"]), -1
)
# `after` was never called, so the rule has nothing to say. Not a violation.
if after_at == -1:
continue
satisfied = any(c["name"] == rule["before"] for c in tool_calls[:after_at])
if not satisfied:
violations.append({"kind": "missing-prerequisite", "tool": rule["before"]})
return violationsThere are three deliberate choices in that code. Other calls sitting in between are fine. Calling before three times is fine. If after never shows up at all, the rule is skipped rather than failed. Together they mean the grader asserts one fact — the policy was checked before the refund — instead of asserting a whole route.
If a trajectory policy lists more than about five prerequisite rules, it is almost certainly over-specified. A real business flow usually has one or two prerequisites that actually matter.
Loop detection and thresholds
Infinite looping is the signature way an agent falls over: it gets a result it does not like, so it retries; the retry returns the same result, so it retries again.
The rule is one sentence: the same tool with exactly the same arguments appeared more than N times.
The arguments half cannot be dropped. Counting by tool name alone misreads "look up ten different orders in sequence" as a loop, which is the most common false positive here. And the arguments have to be serialized with the keys sorted, or the same call written with two different key orders counts as two different calls and the loop stops being detectable at all.
export function callSignature(c) {
// Sort the keys: {a:1,b:2} and {b:2,a:1} must produce one signature, not two.
const parts = Object.keys(c.args)
.sort()
.map((k) => `${k}=${JSON.stringify(c.args[k])}`)
return `${c.name}(${parts.join(',')})`
}
export function detectLoop(toolCalls, maxRepeats) {
const counts = new Map()
for (const c of toolCalls) {
const sig = callSignature(c)
counts.set(sig, (counts.get(sig) ?? 0) + 1)
}
let signature = ''
let maxSeen = 0
for (const [sig, n] of counts) {
if (n > maxSeen) [maxSeen, signature] = [n, sig]
}
return { looped: maxSeen > maxRepeats, signature, maxRepeats: maxSeen }
}import json
from collections import Counter
def call_signature(c):
# Sort the keys: two key orderings must produce one signature, not two.
parts = [f"{k}={json.dumps(c['args'][k])}" for k in sorted(c["args"])]
return f"{c['name']}({','.join(parts)})"
def detect_loop(tool_calls, max_repeats):
counts = Counter(call_signature(c) for c in tool_calls)
signature, max_seen = ("", 0)
for sig, n in counts.items():
if n > max_seen:
signature, max_seen = sig, n
return {
"looped": max_seen > max_repeats,
"signature": signature,
"maxRepeats": max_seen,
}That leaves the value of N. It belongs to the task. It cannot be a global constant.
The reason is plain. For a task whose lookup returns an answer on the first try, the same query three times over is unambiguously a loop. For a task that polls an asynchronous job, a dozen identical status checks is the normal, correct behavior. Both kinds of task live in the same suite, and a single global threshold only lets you choose between missing half the real loops and failing half the healthy tasks.
Step and spend budgets
A budget upgrades "it got done" into "it got done within budget."
The mechanics are simpler than the previous two: total tool calls in one trajectory must not exceed N, total spend must not exceed M cents. Over either line is a failure, and the reason says which line and by how much.
Simple does not mean unimportant. Cost regressions are the easiest kind of degradation to sneak into production: you change a prompt, the pass rate does not move at all, but the mean step count goes from three to eight and nobody looks at the bill for another quarter. A budget assertion is the only thing that can stop that before the merge, and Day 6 wires it into the gate.
Simulated users
Every task so far has been single-turn: the user says one thing, the agent does the work, done.
Real support does not look like that. The user's opening message is usually incomplete, and when the reply is unsatisfying they push back — and pushing back is where things break. The loop in today's lab only appears in a multi-turn setting:
User: Refund order B2002 for me.
Agent: Order B2002 is past the 30-day refund window and cannot be refunded.
User: That can't be right. Please check order B2002 again, I'm sure it's still in window.
Agent: (looks up the order four times in a row with identical arguments)Nobody says "check again" in their opening message, so a single-turn suite structurally cannot reach that path. Automating multi-turn means having something that can play the user.
The cheapest thing that works is rule-based: look at whether the agent's last reply contained "cannot be refunded," and if so, send one fixed follow-up. It is enough, and it is perfectly reproducible.
But a simulated user brings biases of its own, and this is the part to remember:
| Bias | Where it comes from | Consequence |
|---|---|---|
| Phrasing too uniform | A rule-based user pushes back with the same sentence every time | You only discover one failure mode; the score reads optimistic |
| Leaking the answer | A model playing the user is handed the full task setup, and tends to say the answer out loud in its follow-up | The agent under evaluation is taking an open-book exam; optimistic and much harder to notice |
| Infinitely patient | The simulated user never hangs up and never changes its mind | Real conversations contain abandonment signals; this erases them |
All three point the same direction: they all inflate the score.
Which fixes the correct reading of a multi-turn evaluation: it is a tool for finding failures, not a tool for estimating success rates. Do not publish the pass rate a simulated user produced. Do take the loop it found and go fix it.
Failure attribution
The last step, and the one that makes every metric above actually usable.
Suppose a suite finishes and reports a 33 percent success rate. What do you do next? That number cannot direct any action.
Bucket the failures by cause and it turns into this:
-- failure attribution (67 of 100 trials failed) --
loop 29 43.3%
forbidden-tool 20 29.9%
missing-prerequisite 14 20.9%
outcome-mismatch 4 6.0%Now the next move is obvious: fix the looping first, it is four out of every ten.
Bucketing has one rule of discipline: one failure goes in exactly one bucket, ordered so that the cause closest to the root wins. Today's order is crash, forbidden tool, missing prerequisite, loop, over budget, outcome mismatch. Outcome mismatch sits last not because it is unimportant but because it is usually the consequence of the ones above it. Put it first and the attribution table collapses into a single column reading "the result was wrong," which is the same as having no attribution at all.
Source Reading
Both of today's sources circle one question: how do you design a benchmark for multi-turn tool interaction.
tau2-bench is currently the customer-service multi-turn agent benchmark whose source is most worth reading. Focus on the user side: the simulated user is not a loop somebody threw together, it has an explicitly defined policy, explicit termination conditions, and a reproducible seed. Those three properties are exactly what the few dozen lines of simulated user in today's lab are reaching for.
The tau-bench paper (arXiv 2406.12045) is its predecessor, and the source of the pass^k metric from Day 1. Reading it again today, shift your attention to the environment design in section three: it writes the business policy as a standalone document the agent is required to obey, and then evaluates whether the policy was obeyed rather than whether the answer sounded right. That is the same idea as today's prerequisite checks.
One thing to notice while reading, because it is itself a teaching point for today: the original leaderboard is frozen on an early model set, newer models are scored against the successor, and the successor changed the grading rules for one of its business domains in a minor release, which means scores from before and after that release are not comparable. The ranking screenshots circulating online mostly do not say which version they came from.
The lesson is not that this benchmark is unreliable. It is that any score has to carry its suite version, or it is just a number. Day 6 turns that into a mandatory step via the baseline snapshot.
Hands-On Lab
Today you write graders/trajectory.ts and agent/simulated-user.ts, four TODOs in total: tool-sequence checking, loop detection, failure attribution, and turning a single-turn run into a multi-turn one.
One design choice is worth stating up front: policy is pure data, looked up by task id. Thresholds and rules live in a PolicyTable rather than scattered through the code, so they go into git alongside the benchmark set and can be reviewed. This is the same tradeoff as Day 1's "tasks are pure data, graders are registered by name."
The way the simulated user attaches is also deliberately arranged to avoid touching the kernel: it does not introduce a second runner, it wraps the single-turn target into a multi-turn target. From the outside it is still run(task, world) returning one transcript, so Day 1's runner, both probability metrics, and today's three trajectory graders all work without a single line changed.
Interview Questions
Four questions today, and the first two are where the follow-ups land hardest.
The first one hands you a real scenario: the results are all correct and the cost has gone up tenfold, so how does an evaluation system notice. Answering "add cost monitoring" earns half credit. What the question wants is why this is structurally invisible at the outcome layer, and which layer the budget assertion belongs on.
The second is a position question: should "the tool-call order must match exactly" go into a grader. Saying yes or saying no matters far less than being able to describe the concrete mechanism by which it kills a healthy run.
Checklist and Tomorrow
By the end of today you should be able to:
- Name three failure classes only a trajectory reveals, with an automatically checkable rule for each
- Explain why "called something forbidden" hides behind a correct outcome more easily than "skipped a required call"
- State the boundary of sequence checking: assert the prerequisites as a partial order, never a full route
- Write the loop signature algorithm, and say why the arguments have to be key-sorted
- Explain why the loop threshold must belong to the task, with an example where a global threshold is guaranteed wrong
- Name the three simulated-user biases and note that all three push the score the same way
- Get all eight assertions green with
MOCK=1 pnpm selftest, and run one mutation check by hand - Read the failure attribution table and say what it gives you that a single success rate does not
Tomorrow is D5, Observability: Putting a Flight Recorder on Every Run. Today's transcript is the record an offline evaluation keeps; tomorrow the same idea moves to production — instrumenting each run against the OpenTelemetry generative AI semantic conventions and aggregating cost and latency per task and per trial. There is a structural turning point waiting there: the evaluation result is itself a first-class piece of telemetry in those conventions, and that is the moment offline evaluation and production monitoring become one thing.
Interview questions
An agent ships with all outcomes correct, but its average cost has gone up tenfold. How would your evaluation system catch that?一个 Agent 上线后结果全对,但平均成本翻了十倍。你的评估体系怎么才能发现这件事?
Common in ChinaCommon overseasIntermediate#evaluation#trajectory#costHow to reason about it · think before answering
- This tests whether you understand that outcome-based evaluation has a structural blind spot. Answering 'add a cost alert' earns half credit - that is remediation after the fact, while the question asks why the evaluation itself missed it.
- State the mechanism: an outcome grader reads the final state of the environment, and cost is not part of that state. The refunds table holds an order id and an amount, not the tokens spent or the number of tool calls. No matter how strict your outcome assertions are, they cannot in principle detect a tenfold cost increase. That is a coverage gap, not an oversight.
- The fix is to assert on the trajectory itself: total tool calls per trajectory below N, total spend below M. These sit in a layer parallel to the outcome grader, and either breach fails the trial. It must be a failure rather than a warning - warnings in CI are equivalent to nothing.
- Where the thresholds come from: the current baseline. Measure the present distribution over a batch of trials and set the ceiling slightly above today's 95th percentile rather than picking a round number. That tolerates normal jitter while going red as soon as the mean shifts up.
- Add why this class of regression slips into production so easily: cost regressions change nothing a user can see. A new prompt, one more reflection round, longer tool descriptions - the pass rate is unchanged, steps go from three to eight, and the bill surfaces at month end. Without budget assertions the pipeline stays green the whole way.
- Expected follow-up: steps or spend as the gate? Both, because they diverge. Swapping in a pricier but smarter model lowers steps and raises unit price. Watching only steps misses the price increase; watching only spend misses the extra wandering caused by weaker reasoning.
分析过程 · 先想清楚再作答
- 这题考的是「知不知道结果态评估有结构性盲区」。回答「加一个成本监控告警」只能拿一半分——那是发现之后的补救,题目问的是评估体系本身为什么漏掉了它。
- 先把机制说清楚:结果态评分器的输入是环境的最终状态,而成本不在最终状态里。退款记录表里只有订单号和金额,没有「这次花了多少 token、调了几次工具」。所以无论你把结果态断言写得多严,它在原理上都判不出成本翻十倍这件事。**这是覆盖不到,不是写漏了。**
- 正确的做法是给轨迹本身写断言:一条轨迹的工具调用总数不超过 N、总花费不超过 M。这两条挂在与结果态平行的一层,任一超标就判这次试次失败。注意它必须是**失败**而不是警告——警告在 CI 里等于没有。
- 阈值从哪来:从当前基线来。先跑一批试次统计出现在的分布,取一个略高于当前 p95 的数作为上限,而不是拍一个整数。这样它既能容忍正常抖动,又能在均值整体上移时立刻报红。
- 还要补一句为什么这类退化特别容易溜进生产:**成本回归不改变任何用户可见的行为。** 换个提示词、多加一轮反思、把工具描述写长一点,成功率一点没掉,步数从 3 涨到 8,账单要到月底才有人看。没有预算断言的话,评估流水线全程报绿。
- 可预期的追问是「那步数和花费该选哪个当闸门」。答案是两个都要,因为它们会分叉:模型换成一个更贵但更聪明的,步数会降而单价会升。只看步数会漏掉换模型带来的涨价,只看花费会漏掉逻辑变笨带来的绕路。
Key points
- Cost is absent from the outcome state, so an outcome grader cannot detect this class of regression at all.
- Add a parallel layer of trajectory assertions: a step ceiling and a spend ceiling, failing the trial rather than warning.
- Derive thresholds from the current baseline distribution, slightly above p95, not from a round number.
- Cost regressions change nothing user-visible, so without budget assertions the pipeline stays green.
- Track both steps and spend: a pricier model moves them in opposite directions and either alone leaves a hole.
答题要点
- 结果态里根本不含成本,所以结果态评分器在原理上覆盖不到这类退化。
- 给轨迹写平行的一层断言:步数上限与花费上限,超标判失败而不是告警。
- 阈值从当前基线的分布取,略高于 p95,而不是拍一个整数。
- 成本回归不改变任何用户可见行为,所以没有预算断言时流水线会全程报绿。
- 步数与花费都要盯:换更贵的模型会让两者反向变化,只看一个都会漏。
Would you assert that an agent's tool-call sequence must exactly match a reference workflow? Give your reasoning.你会把「工具调用顺序必须与参考流程完全一致」写进评分器吗?说出你的理由。
Common in ChinaCommon overseasIntermediate#evaluation#trajectory#gradersHow to reason about it · think before answering
- This is a position question, but the credit is not in the position. Either answer can score; what matters is naming the concrete mechanism of false failures and proposing a workable middle ground.
- Why a total-order assertion is wrong: part of an agent's value is finding solutions the designer did not anticipate - one extra order lookup to confirm the amount (more careful), two independent lookups issued concurrently (faster), reading a cached policy conclusion and skipping a call (cheaper). A strict sequence assertion fails all three, and all three are improvements. A grader that punishes improvement is the worst kind of defect: it pushes the team toward a dumber but more obedient implementation.
- There is a subtler cost too. Total-order assertions go red en masse on every model upgrade for reasons unrelated to quality. The team then either spends days updating reference workflows or disables the whole class of assertion, losing the real problems it would have caught.
- You still cannot assert nothing, or a refund issued without a policy check goes unnoticed. The middle ground is to assert only necessary precedence, and as a partial order: before the first issue_refund, check_policy must have appeared at least once. Intervening calls are fine, three policy checks are fine, and if no refund happened the rule is simply skipped.
- A simple test for whether a precedence rule belongs in the policy: if it were violated, would a real loss follow? Refunding without a policy check loses money, so it belongs. Whether the order lookup precedes the policy check has no consequence, so it does not. More than about five precedence rules in one policy almost certainly means overreach.
- Expected follow-up: if the order was wrong but the outcome was right, does the trial pass? Report them separately - outcome passed, sequence failed, each recorded on its own. Collapsing them into one score destroys exactly the information you need, which is that this run got lucky.
分析过程 · 先想清楚再作答
- 这是一道立场题,但分不在立场上——答「会」或者「不会」都能拿分,关键是能不能说出误杀的**具体机制**,以及给出一个能落地的中间方案。
- 先说为什么不该写死全序。Agent 的价值有一部分正来自它会找到设计者没想到的解法:为了确认金额多查一次订单(更谨慎)、把两次独立查询并发发出(更快)、从缓存里直接读到结论省掉一次调用(更省)。这三种在全序断言下**全部判失败**,而它们全是改进。你的评分器在惩罚改进,这是最坏的一种评估缺陷——它会把团队推向一个更笨但更听话的实现。
- 还有一个更隐蔽的代价:全序断言会在模型升级时大面积变红,而红的原因与质量无关。于是团队要么花大量时间逐条更新参考流程,要么干脆把这类断言整体关掉,连同它本来能抓到的真问题一起。
- 但也不能一条都不判,否则「没查政策就退款」这种真问题没人管。中间方案是**只判必要的前置关系,而且判偏序**:断言「第一次 issue_refund 之前,check_policy 至少出现过一次」。中间夹了别的调用不算违规,查了三次也不算,压根没退款时这条规则直接跳过。
- 判断一条前置关系该不该写进去,有个简单标准:**如果它被违反,会不会导致一次真实的损失?** 不查政策就退款会退错钱,该写;先查订单再查政策还是反过来,没有任何后果,不该写。一份策略里超过五条前置关系,基本可以确定写多了。
- 可预期的追问是「那顺序错了但结果对了,到底算不算通过」。答案是分开报:结果态通过、序列不通过,两个分数各自记录。合成一个总分会丢掉信息——你需要知道的恰恰是「这次是蒙对的」。
Key points
- Do not assert a total order: an extra lookup, concurrent calls, or a cache shortcut all get failed, and all are improvements.
- Total-order assertions also go red wholesale on model upgrades and end up disabled, taking the real findings with them.
- The middle ground is necessary precedence as a partial order: a policy check somewhere before the first refund.
- The test is whether a violation causes real loss; more than about five precedence rules means overreach.
- Report outcome and sequence as separate scores - merging them hides the fact that a run got lucky.
答题要点
- 不写死全序:多查一次、并发查询、走缓存捷径都会被误杀,而它们全是改进。
- 全序断言还会在模型升级时大面积变红,最终被整体关掉,真问题一起丢掉。
- 中间方案是只判必要前置且判偏序:第一次退款之前查过政策即可。
- 取舍标准是「违反了会不会造成真实损失」,超过五条前置关系基本是写多了。
- 结果态与序列两个分数分开报,合成总分会丢掉「这次是蒙对的」这条关键信息。
How do you automatically detect that an agent is stuck in a loop? Give your rule and its false-positive risks.怎么自动判定一个 Agent 陷入了死循环?给出你的规则,以及它的误判风险。
Common in ChinaCommon overseasIntermediate#evaluation#trajectory#loop-detectionHow to reason about it · think before answering
- The discrimination here is all in the details. 'More than three calls to the same tool' is the most common answer and it is wrong - it fails a large class of perfectly normal tasks.
- The correct criterion is the same tool with identical arguments occurring more than N times. The arguments clause is essential: looking up ten different orders is ten lookup calls with ten different argument sets, which is normal batch work; four lookups of the same order id is a loop.
- One implementation detail you must hit: sort the argument keys before serializing them into a signature. Serializing the object directly means the same call written with two key orders produces two different signatures, and the loop goes undetected - a silent failure that raises no error.
- N must be set per task, and that is the real point of the question. A task that resolves in one lookup is looping if it repeats three times; polling an asynchronous job legitimately requires a dozen checks. With both kinds in one suite, a single global threshold forces a choice between missing half the loops and failing half the normal tasks.
- Three main false-positive risks: legitimate retries after a transient failure, which argues for including success/failure in the signature; idempotent polling, handled by per-task thresholds; and arguments containing timestamps or random ids, which make every signature unique and hide loops entirely - those volatile fields must be stripped when computing the signature.
- Expected follow-up: what other shapes of looping exist? Semantic loops, where tools and arguments differ but the agent oscillates between two states. Those are caught by the step budget instead - you cannot prove it is a loop, but you can prove it blew the budget, and for evaluation purposes the conclusion is the same: this trial does not pass.
分析过程 · 先想清楚再作答
- 这题的区分度全在细节上。答「同一个工具调用超过三次就是循环」是最常见的答案,也是错的——它会把一大批正常任务误判成死循环。
- 正确的判据是**同一个工具加上完全相同的参数**出现超过 N 次。参数这半句不能省:依次查询十个不同的订单,是十次 lookup 调用但参数各不相同,它是正常的批量操作;连续四次查同一个订单号,才是循环。
- 实现上有个必须踩到的细节:参数要**按键排序后**再序列化成签名。直接对参数对象做 JSON 序列化的话,同一次调用写成两种键顺序会算出两个不同签名,于是循环恰好检测不出来——而这是一个不会报错的静默失效。
- N 的取值必须**按任务定**,这是这题真正的考点。查一次就有结果的任务,重复三次一定是循环;而轮询一个异步任务的状态,本来就要查十几次才等到完成。同一套评估里两种任务共存,一个全局阈值只能在「漏掉一半死循环」和「误伤一半正常任务」之间挑一个。
- 误判风险主要有三类:① 合法的重试——网络失败后重试同一个调用是正确行为,所以理想情况下签名里应该带上返回是否成功;② 幂等的轮询,靠按任务调阈值解决;③ 参数里带了时间戳或随机 id,导致每次签名都不同,循环被完全漏掉,这一类要在算签名时显式剔除易变字段。
- 可预期的追问是「除了重复调用,还有什么循环形态」。答案是语义层面的循环:工具和参数都不同,但 Agent 在 A 和 B 两个状态之间来回横跳。这种要靠步数预算兜底——判不出它是循环,但能判出它超了预算,而对评估来说结论是一样的:这次试次不合格。
Key points
- The rule is the same tool plus identical arguments exceeding N, not the same tool exceeding N.
- Sort argument keys when computing the signature, or differing key order silently hides the loop.
- Set the threshold per task: one-shot lookups and polling workflows cannot share a number.
- Three false-positive sources: legitimate retries, idempotent polling, and volatile fields such as timestamps or random ids.
- Semantic loops that oscillate between states are not detectable this way; the step budget catches them instead.
答题要点
- 判据是「同一工具 + 完全相同参数」超过 N 次,不是「同一个工具」超过 N 次。
- 算签名时参数必须按键排序,否则键顺序不同会让循环静默漏检。
- 阈值按任务定:一次查询就有结果的任务与需要轮询的任务不能共用一个数。
- 三类误判:合法重试、幂等轮询、参数里带时间戳或随机 id 导致签名永不重复。
- 语义循环(在两个状态间横跳)检测不出来,靠步数预算兜底。
How do you automate evaluation of multi-turn conversations, and what problems does a simulated user introduce?多轮对话场景怎么做自动化评估?模拟用户会带来什么问题?
Common in ChinaCommon overseasDeep dive#evaluation#multi-turn#simulated-userHow to reason about it · think before answering
- The question has two halves and the second is the discriminator. Almost everyone can answer 'write a simulated user'; the score depends on naming the systematic biases it introduces.
- Why it is necessary: real users open with incomplete requests and then push back, and the pushback is where agents break - context grows, constraints get diluted, tools get called repeatedly. Single-turn evaluation structurally cannot reach that path, because nobody says 'check again' in their opening sentence.
- Two implementations exist. A rule-based user keys off phrases in the agent's last reply, giving perfect reproducibility at zero cost. A model-based user has another model play the customer, giving varied phrasing that is closer to reality.
- The rule-based bias is uniformity: it pushes back with the same sentence every time, so the evaluation surfaces exactly one failure mode. Real users express the same intent a hundred ways, and some of those phrasings take entirely different paths.
- The model-based bias is subtler - leakage. The model playing the user holds the full task setup and readily gives the answer away in its follow-up ('check clause three of the policy table'), so the agent under test is taking an open-book exam. Both kinds share a third bias: they are too patient. They never hang up, change their mind, or get angry, and abandonment in real conversations is itself an important signal.
- Crucially all three biases point the same way: they inflate the score. So multi-turn evaluation is a tool for finding failures, not for estimating success rates. Do not publish the simulated-user pass rate, but do go fix the loop it uncovered. Reporting a rate requires calibration against replayed real conversations or human sampling.
- Expected follow-up: how do you make multi-turn evaluation reproducible? Three things - seed the simulated user's randomness, hard-cap the number of turns as a termination condition, and rebuild the environment between trials. Note that within a single conversation the environment is shared across turns; the isolation boundary is the trial, not the turn.
分析过程 · 先想清楚再作答
- 这题分两半,后半半才是考点。前半半答「写一个模拟用户」几乎人人会答,能不能说出它引入的系统性偏差才分高下。
- 先讲为什么非做不可:真实用户第一句话往往不完整,会追问,而追问才是最容易翻车的地方——上下文变长、约束被冲淡、工具被反复调用。**单轮评估在结构上碰不到这条路径**,因为没有人会在第一句话里说「你再查一次」。
- 实现上有两种模拟用户。规则型看 Agent 上一句回复里的关键词决定下一句说什么,优点是完全可复现、零成本;模型型让另一个模型扮演用户,优点是表达多样、更接近真实。
- 规则型的偏差是**表达过于一致**:它每次都用同一句话追问,于是评估只能发现一种失败模式。真实用户会用一百种说法表达同一个意思,其中某些说法会触发完全不同的路径。
- 模型型的偏差更隐蔽,是**泄题**:扮演用户的模型拿到的是任务的完整设定,它很容易在追问里把答案说出来(「你去查一下政策表第三条」),于是被评的 Agent 在开卷考试。另外两种模拟用户共有一条偏差:它们**太有耐心**,不会挂电话、不会改主意、不会骂人,而真实对话里的放弃行为本身是一个重要信号。
- 关键是这三条偏差**方向一致,都让分数偏高**。所以结论是:多轮评估是发现失败的工具,不是估计成功率的工具。模拟用户跑出的成功率不要直接对外报,但它抓出来的那条死循环可以直接拿去修。要报成功率就得用真实对话回放或人工抽查来校准。
- 可预期的追问是「怎么让多轮评估可复现」。答案是三件事:模拟用户的随机源要固定种子、终止条件要写死最大轮数、每次试次之间环境必须重建;而**同一次会话内部的轮次之间环境是共享的**——隔离的边界是试次,不是轮次。
Key points
- It is necessary because the follow-up path is structurally unreachable in single-turn evaluation and is where agents break.
- A rule-based simulated user is reproducible but too uniform, surfacing only one failure mode.
- A model-based simulated user leaks the answer, since it holds the full task setup.
- Both are too patient: they never abandon or change their mind, erasing a real signal.
- All biases inflate the score, so use multi-turn evaluation to find failures, not to report success rates.
答题要点
- 必须做的理由:追问路径在单轮评估里结构性地碰不到,而它正是最容易翻车的地方。
- 规则型模拟用户可复现但表达过于一致,只能发现一种失败模式。
- 模型型模拟用户会泄题:它拿着完整任务设定,容易在追问里把答案说出来。
- 两者共有的偏差是太有耐心:不会放弃、不会改主意,抹掉了真实的放弃信号。
- 三条偏差方向一致地抬高分数,所以多轮评估用于发现失败,不用于报成功率。