Jev 问题设计与状态管理实战
判断模型会放大你喂给它的东西:一道边界清晰的 jev api 问题能让你拿到稳定、带 confidence 的答案用几个月;一道含糊的问题则让答案随输入的细微变化而漂移。所以在 jev tutorial 的集成实践里,问题设计是杠杆最高的一环。这篇讲怎么把判断题、选择题、打分题写稳,怎么定义模型真正能执行的边界,调用之间怎么传状态,置信度如何与状态配合,以及怎么在 n8n 这类自动化流程里做状态持久化。
TL;DR: 显式说清答案空间,用具体规则或示例定义边界,给棘手情况配 2–5 个 few-shot 示例,上下文以明确文本随问题传入。下游调用用 confidence 字段做门控。如果一个问题两次回答结果不一致,先改问题,再怀疑模型。
稳定问题的解剖
一道稳定的问题有四个部分,按这个顺序:
- 题型。 “Judgment question:”、“Choice question:” 或 “Scoring question (1-10):“——先报题型,答案空间才无歧义。
- 判定标准。 一句话说清什么算 yes、哪个选项胜出、高分意味着什么。
- 边界。 边缘情况算什么,写成规则或示例。
- 输入。 待判对象,明确分隔。
对比弱版本和强版本:
弱:"Is this review bad?"
强:"Judgment question: should this product review be hidden for
violating the no-promotional-links policy? The policy prohibits
links to sellers, coupon codes, and contact info. Mentions of the
product's own brand do not count. Review: \"...\""
弱版本在邀请漂移:“bad” 没有答案空间。强版本报了题型、给了标准、点了边界、隔开了输入。它的响应——示例数据(example fixture),正式字段名以官方文档为准——长这样:
{
"answer": "no",
"confidence": 0.93,
"rationale": "The review mentions the product's own brand only, which the policy explicitly excludes from violations."
}
选择题的选项枚举
选择题成也选项列表,败也选项列表。三条规则:
- 列表要么穷尽,要么加逃生选项。 如果”其他”是合法结局,就把它枚举出来;否则模型被迫选一个错误的桶。
- 每个选项一句话定义,保证互斥。 “billing——关于扣款、退款、发票的争议”比光秃秃的 “billing” 强得多。
- 选项数量控制住。 大约超过一打选项,准确度就下滑;考虑拆成两段式选择题。
import json, os, requests
prompt = (
"Choice question: which queue does this ticket belong to? "
"Definitions: billing - charges, refunds, invoices; "
"technical - product errors and bugs; "
"account - login, permissions, profile. "
"Ticket: \"...\""
)
resp = requests.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"},
json={
# Confirm the exact model slug on the OpenRouter model page
"model": "typesafe/jev-1.13",
"messages": [{"role": "user", "content": prompt}],
},
timeout=30,
)
route = json.loads(resp.json()["choices"][0]["message"]["content"])
一句话定义把光秃秃的标签变成了边界;上线前去 OpenRouter 模型页确认确切的 model slug。解析出的 route 是示例数据(example fixture)——响应形态为示例,正式字段名以官方文档为准,形如 {"answer":"technical","confidence":0.9,"rationale":"..."}。
few-shot 示例在边界处最值钱
抽象规则覆盖分布的中间,示例覆盖边缘。针对你被坑过的场景,在问题里放 2–5 个示例:
Examples:
- "Buy now at deals.example, 50% off!" -> hide (promotional link)
- "This product broke after two weeks, very disappointed." -> keep (genuine complaint)
- "Great product, contact me at me@example.com for bulk orders." -> hide (contact info)
Now judge: Review: \"...\"
每个示例都是被具体化的边界。答案开始漂移时,为漂移的那一类补一个示例,而不是无限扩写规则。规则段落写得再长,通常也比不上一个选得准的示例——模型从具体实例推断规则的能力强过理解抽象描述。
如果你发现自己在堆超过五个示例,通常是信号:判定标准那句话还没说清楚。先退回去把标准句收紧,再考虑加示例。
多轮/多请求里的 jev state 生命周期
jev api 调用是单轮设计,状态不会在调用之间持久化——由你来搬运。理解 jev state 的生命周期,是把集成做稳的前提。
调用范围的状态随一次请求生死。当前的对话摘要、管线阶段、上一次 jev 的答案——全是调用范围的。写进问题文本,不要在别处缓存。
会话范围的状态跨越一个用户会话内的多次调用:累积的判断结果、本次对话的违规计数、用户在更早轮次声明的偏好。放在你的应用层(用 session ID 做键的内存字典对短生命周期会话足够),每次调用时把相关切片注入问题。
账户范围的状态是长期的:订阅层级、历史升级率、固定的政策豁免。属于数据库。每次管线运行取一次,嵌进状态块。
经验法则:jev api 只知道你在当前请求里告诉它的东西。 任何需要跨调用保持一致的内容,都得由你的代码管理。
def build_prompt(criterion, item, session_state, account_state):
state_block = (
f"Account context: {account_state['tier']} plan, "
f"violations this month: {account_state['violation_count']}.\n"
f"Session context: {session_state['prior_judgments_summary']}\n"
)
return (
f"Judgment question: {criterion}\n"
f"{state_block}"
f"Input: {item}"
)
状态块保持简短、只陈述事实。超过两三句话就压缩——冗长的状态会挤占模型处理输入的有效注意力。
什么时候状态应该随问题传入,什么时候该由服务端持有? 状态直接影响当次判断时,写进问题。状态跨越多次调用共享但很少真正改变单次答案时(比如用户的语言偏好、全局开关),放服务端、调用前注入一次就够了,过度传递状态会推高 token 成本,还可能把真正的输入内容挤走。
问题设计:粒度、数量与冲突
一个管线步骤里应该有几道 jev 问题?实践答案是:一次调用,一个判断。复合问题——“这是垃圾邮件,并且需要升级吗?“——逼迫模型在单次答案里打包两个独立决策,哪个子标准出了问题都很难追溯,迭代成本高。
一问题、一标准、一答案。 需要两个判断就发两次请求。增加的延迟,通常比省下来的排查时间值得。
问题粒度同样关键。“给这位候选人的沟通能力打分(1-10)“太粗,如果沟通能力包含书面清晰度、响应速度、压力下的表达——最好分维度各打一次,聚合逻辑留在你的代码里,可见、可调整。
冲突问题出现在两道标准有交叉但不完全一致的时候:同一篇文档既问”相关性”又问”质量”,可能产生矛盾信号,因为相关性里本来就有一部分质量的成分。把标准定义成正交的——相关性只看主题契合度;质量只看写作水准,与主题是否正确无关——冲突就消失了。
问题重复有时用作一致性校验:用略有差异的措辞问同一个事情,看两次结果是否一致。这个方法有效,但贵。更便宜的一致性检查是给 few-shot 示例集做版本管理——如果模型在你的示例集上的答案在两次部署之间发生了偏移,说明问题或模型变了,升版前先审计:
EXAMPLE_FIXTURES = [
{"input": "Great product!", "expected_answer": "keep", "expected_confidence_min": 0.85},
{"input": "Buy at spam.example", "expected_answer": "hide", "expected_confidence_min": 0.90},
]
def smoke_test(prompt_template):
for ex in EXAMPLE_FIXTURES:
result = call_jev(prompt_template.format(input=ex["input"]))
assert result["answer"] == ex["expected_answer"]
assert result["confidence"] >= ex["expected_confidence_min"]
每次修改问题后都跑一遍这个冒烟测试再部署,不到一秒,能拦住大多数回归。
置信度(confidence)与 state 的配合:低置信度时如何回退与再问
confidence 字段不是装饰。它是模型在告诉你它抵达了你提供的信息边界。置信度下降时,正确反应通常是补充上下文后重问,而不是接受答案继续推进。
用置信度为下游状态做门控。 在把 jev 答案嵌入下一次调用的状态块之前,先检查:
CONFIDENCE_THRESHOLD = 0.75
def safe_embed_prior_answer(prior_result, state):
if prior_result["confidence"] >= CONFIDENCE_THRESHOLD:
state["prior_judgment"] = (
f"Earlier judgment: {prior_result['answer']} "
f"({prior_result['confidence']:.2f}) because "
f"{prior_result['rationale']}"
)
else:
state["prior_judgment"] = "Earlier judgment: unclear (low confidence — omit from this call)"
return state
把低置信度答案当确定信号传给下游,是最常见的 jev state bug 之一。下游调用把它当可靠来源使用,而实际上它不是。
补示例后重问是置信度低于阈值时的第一步动作。针对产出低置信度答案的那类输入,加一个具体的 few-shot 示例,然后重跑同一次调用:
def reask_with_example(original_prompt, low_conf_item, correct_answer, explanation):
extra_example = (
f"Additional example: \"{low_conf_item}\" -> {correct_answer} ({explanation})\n"
)
return extra_example + original_prompt
补示例后仍然低置信度,转人工审核。不要自动叠加超过一次重问——同一条输入连续两次低置信度,说明问题标准需要重写,不是再加示例就能解决的。
置信度与问题数量的相互作用: 如果你顺序发出十道问题,每道有 5% 概率低于阈值,大概有一半管线运行会至少撞上一次低置信度。在上生产前就把降级路径设计好。
多轮与多请求场景的状态传递
多轮交互和多步骤自动化管线都需要显式的状态管理,模式略有差异。
多轮(交互式): 用户在每步输入。状态包含压缩过的对话历史:
Context: User is screening a resume for a junior engineer role.
Prior turns:
- Turn 1: scored technical skills 7/10 (confidence 0.91)
- Turn 2: scored communication 6/10 (confidence 0.88)
Now judge: Scoring question (1-10): overall hire recommendation.
Candidate summary: "..."
注意只有前几次的分数和置信度向前传递,而不是完整的前几次 prompt。压缩而非拼接,保持状态块简短。
多请求(批量管线): 每条输入是独立的,但管线级别的上下文共享。维护一个不随输入数量增长的共享上下文字典:
pipeline_context = {
"policy_version": "v2.3",
"campaign_id": "camp_abc",
"reviewer_tier": "enterprise",
}
def process_item(item, pipeline_context):
prompt = build_prompt(
criterion=CRITERION,
item=item,
shared_context=pipeline_context,
)
return call_jev(prompt)
如果某个管线步骤的结果会影响后续步骤——比如路由决策改变了下一次调用的判定标准——只传 answer 和一行 rationale,不要搬整个响应对象。
与 n8n/自动化流程接线时的 state 持久化与幂等
把 jev api 调用接进 n8n 或类似的自动化平台时,状态持久化和幂等性变成生产级关切。
持久化: n8n 工作流可能在执行中途失败。如果 jev 调用成功但后续写入失败,从头重跑会把这次调用计两次。用 n8n 内置的执行数据在每次成功的 jev 调用后做检查点:
{
"item_id": "review_9182",
"jev_answer": "hide",
"jev_confidence": 0.93,
"jev_called_at": "2026-09-28T08:42:00Z"
}
在继续工作流之前,先用数据库节点把这个检查点写进去。重试时检查 jev_called_at 是否已经为该条目设置——设置了就跳过 jev 调用,直接用存好的答案。
幂等性: jev 问题本身是幂等的——同样的问题加同样的输入返回同样的答案。风险在于你附带的状态。如果失败和重试之间会话状态块发生了变化(因为另一个事件更新了它),答案可能随之变化。调用时对状态块做快照,连同答案一起存下来,之后能审计是哪个状态产出了哪个结果:
checkpoint = {
"item_id": item_id,
"prompt_snapshot": prompt, # 发出的完整字符串
"answer": result["answer"],
"confidence": result["confidence"],
"rationale": result["rationale"],
"called_at": datetime.utcnow().isoformat(),
}
db.upsert("jev_checkpoints", checkpoint, conflict_on="item_id")
n8n 具体模式: 用 “Set” 节点从上游节点组装状态块,再接 HTTP Request 节点调用 jev api。这让状态构建过程在工作流图里可见,比塞进 Function 节点更容易排查:
[取账户数据] → [取会话状态] → [Set: 拼状态块] → [HTTP: jev api 调用] → [Set: 写检查点] → [后续操作]
HTTP 节点失败时,n8n 从该节点重试;账户数据和会话状态已经在执行上下文里了,不需要重新拉取。
常见错误与 debug 路径
症状:上周还稳定的答案现在开始漂移。 最可能的原因是输入分布变了,不是模型变了。记录一批输入和它们对应的答案,在低置信度的案例里找新出现的模式。为每个新模式补一个 few-shot 示例。
症状:某一类输入的置信度稳定在 0.5 附近。 问题边界对这一类输入本就含糊——模型没有坏,它在告诉你标准对这类情况无法清晰地区分 yes 和 no。收紧标准句,或者拆成两道问题。
症状:jev api 调用成功,但解析出的答案缺少预期字段。
你在解析一个不完整的缓存响应或降级响应。先确认 Content-Type 是 application/json,choices[0].message.content 是合法的 JSON 字符串再解析。网络代理或限流响应有时返回非 JSON body,json.loads 会直接抛异常。
症状:上一个会话的状态块意外影响了当前结果。 会话 ID 在碰撞,或者你在跨请求复用同一个状态字典没有清空。用 session ID + 请求时间戳的组合作为每个状态字典的键。调用范围的键在每次调用开始时显式清掉。
症状:n8n 工作流重试时对同一条输入调用了两次 jev。 检查点写入在 HTTP 节点之后、某个可能失败的转换步骤之前。把检查点写入移到紧跟 HTTP 节点的下一步,在任何转换之前。
多语言评论审核的 case 展示了枚举政策边界与 few-shot 的合体用法,横跨多种语言;你的边界定义偶尔会产出 unclear 答案,完整的决策树(包括人工升级阈值和自动重问策略)见《Jev 置信度与兜底策略》。
本文适用版本 Jev 1.13。