锚点项目:浙大 X-Lab 能源 Agent Harness、符号化建模 DSL、声明式任务路由 每题包含:问题 → 标准答案 → 核心代码 → 追问应对
一、Agent 架构基础
Q1:讲一下你设计的 Agent Harness 整体架构
标准答案:
整套 Harness 分为 6 层,自顶向下:
- 入口层:自然语言指令进入 → 意图分类 + 任务模板匹配,决定走哪条主路径;
- 规划层:Plan-Execute + ReAct 混合——简单任务走 ReAct(边想边做),复杂多步任务走 Plan-Execute(先全局规划,再逐步执行),中间设检查点检测偏离;
- 执行层:有限状态机驱动,每步可在执行中动态推理调整下一步;
- 工具层:Function Calling + JSON Schema 参数校验 + 工具结果按统一结构回注;
- 记忆层:短/中/长三层记忆 + 上下文压缩 + token 配额;
- 观测层:Kafka 异步采集全链路 trace(input/output/工具/耗时/token),离线分析高频 bad case 反哺调优。
核心代码骨架(Python):
class AgentHarness:
def __init__(self, llm, tools, memory, tracer):
self.llm, self.tools, self.memory, self.tracer = llm, tools, memory, tracer
async def run(self, query: str, session_id: str):
trace_id = self.tracer.start(query, session_id)
ctx = await self.memory.load(session_id) # 拉取分层记忆
intent = await self.classify(query) # 意图分类
if intent.complexity == "simple":
result = await self._react(query, ctx, trace_id)
else:
plan = await self._plan(query, ctx, trace_id)
result = await self._execute(plan, ctx, trace_id)
await self.memory.append(session_id, query, result)
self.tracer.end(trace_id, result)
return result
async def _react(self, query, ctx, trace_id, max_steps=8):
history = []
for step in range(max_steps):
thought = await self.llm.think(query, history, ctx)
if thought.action == "finish":
return thought.answer
tool_result = await self.tools.invoke(thought.action, thought.args)
self.tracer.span(trace_id, step, thought, tool_result)
history.append((thought, tool_result))
return "Reached max steps"
追问应对:
-
Q:怎么判断 simple vs complex?
用 LLM 做 zero-shot 分类(system prompt 给 3 个典型例子),输出
{complexity, reason}。判错有兜底——执行 3 步发现 ReAct 没收敛会自动升级到 Plan-Execute。 -
Q:max_steps=8 怎么定的?
拍了一周线上数据的 P95,95% 的 ReAct 任务在 6 步内完成,留 2 步 buffer。超过就强制终止 + 告警,避免死循环烧 token。
Q2:Plan-Execute 和 ReAct 有什么区别?为什么要混合?
标准答案:
| 维度 | ReAct | Plan-Execute |
|---|---|---|
| 决策粒度 | 每步 Thought→Action→Observation | 先全局 Plan,再逐步 Execute |
| 上下文消耗 | 高(每步带全部历史) | 低(执行阶段不重规划) |
| 应对扰动 | 强(每步可调整) | 弱(计划被打破要 Replan) |
| 成本 | 高(多轮 LLM) | 低(1 次 Plan + N 次工具) |
| 适合场景 | 步数少、路径未知 | 步数多、路径可预测 |
混合策略:先判断任务复杂度——单步可解走 ReAct;多步可拆走 Plan-Execute;执行中发现偏离触发 Replan,且只重规划剩余部分而不是从头来过。
核心代码骨架:
async def _plan(self, query, ctx, trace_id):
prompt = f"""把任务拆成步骤,每步给出预期检查点。
任务: {query}
返回 JSON: [{{"step": 1, "action": "...", "args": {{}}, "checkpoint": "..."}}]"""
return await self.llm.json(prompt, ctx)
async def _execute(self, plan, ctx, trace_id):
results, completed = [], []
for step in plan:
result = await self.tools.invoke(step["action"], step["args"])
# 检查点验证:实际结果是否符合预期
ok = await self._verify_checkpoint(step["checkpoint"], result)
if not ok:
# 局部 ReAct 修正
fix = await self._react(f"修正第 {step['step']} 步: {step}", ctx, trace_id, max_steps=3)
if not fix:
# 修正失败 → 仅重规划剩余步骤
remaining = plan[plan.index(step):]
new_plan = await self._replan(query, completed, remaining, ctx)
return await self._execute(new_plan, ctx, trace_id)
results.append(result)
completed.append(step)
return results
追问应对:
- Q:检查点用 LLM 判还是规则判?
优先规则——schema 校验、数值范围、字段存在性,快且准。规则覆盖不到的(如"答案语义是否合理")才让 LLM 判,并且只让 LLM 输出
{ok: bool, reason: str}短回答,省 token。
Q3:工具注册与路由怎么做?
标准答案:
- 注册:每个工具用 JSON Schema 描述
name / description / parameters / returns,启动时注册到中央 ToolRegistry; - 路由:LLM 通过 Function Calling 选工具 → 中间件 jsonschema 校验参数 → 命中分发执行;
- 回注:统一结构
{status, data, error, next_hint},next_hint 是给 LLM 的下一步暗示; - 容错:参数校验失败 → 结构化错误让 LLM 自纠;工具超时 → 走降级或转人工。
核心代码骨架:
from jsonschema import validate, ValidationError
class ToolRegistry:
def __init__(self):
self._tools = {} # name -> (fn, schema)
def register(self, name: str, schema: dict):
def deco(fn):
self._tools[name] = (fn, schema)
return fn
return deco
def list_for_llm(self):
# 给 LLM 的 function calling 描述
return [{"name": n, "description": s.get("description"),
"parameters": s["parameters"]} for n, (_, s) in self._tools.items()]
async def invoke(self, name: str, args: dict) -> dict:
if name not in self._tools:
return {"status": "error", "error": f"unknown tool: {name}"}
fn, schema = self._tools[name]
try:
validate(args, schema["parameters"])
except ValidationError as e:
return {"status": "error", "error": f"schema: {e.message}",
"next_hint": "检查参数类型并重试"}
try:
data = await asyncio.wait_for(fn(**args), timeout=schema.get("timeout", 30))
return {"status": "ok", "data": data}
except asyncio.TimeoutError:
return {"status": "error", "error": "timeout", "next_hint": "工具超时,可尝试降级方案"}
registry = ToolRegistry()
@registry.register("query_device", {
"description": "按设备 ID 查询实时状态",
"parameters": {"type": "object", "properties": {"device_id": {"type": "string"}},
"required": ["device_id"]},
"timeout": 5
})
async def query_device(device_id: str):
return await db.fetch_one("SELECT * FROM device WHERE id=$1", device_id)
追问应对:
- Q:工具数量多了 LLM 选不准怎么办?
三招:① 工具分组(按命名空间),先让 LLM 选组再选具体;② 检索式工具路由,把 query 向量化和工具 description 算相似度,只把 top-K 工具喂给 LLM;③ 历史调用统计权重,常用工具排前面。
Q4:分层记忆是怎么设计的?
标准答案:
- 短期(对话级):当前会话 messages 数组,滑动窗口 + 摘要压缩;
- 中期(用户级):用户偏好、常用工具序列,Redis 缓存,会话开始注入 system prompt;
- 长期(项目级):项目状态、领域知识、历史结论,向量库 + 关系库按需检索;
- 压缩:每层超阈值触发 LLM 摘要,保留关键决策点;
- 配额:每层有 token 上限,超出按 LRU 淘汰或摘要降级。
核心代码骨架:
class LayeredMemory:
def __init__(self, redis, vec_store, llm):
self.redis, self.vec, self.llm = redis, vec_store, llm
self.short_limit = 20 # 短期保留最近 20 条
self.short_token_cap = 2000
async def load(self, session_id: str, user_id: str) -> dict:
short = await self.redis.lrange(f"chat:{session_id}", 0, self.short_limit - 1)
mid = await self.redis.hgetall(f"user:{user_id}:prefs")
long_hits = await self.vec.search(f"project:{user_id}", k=3)
return {"short": short, "mid": mid, "long": long_hits}
async def append(self, session_id: str, role: str, content: str):
await self.redis.lpush(f"chat:{session_id}", json.dumps({"role": role, "content": content}))
await self.redis.ltrim(f"chat:{session_id}", 0, self.short_limit - 1)
# token 超额触发摘要
total = await self._token_count(session_id)
if total > self.short_token_cap:
await self._compress(session_id)
async def _compress(self, session_id: str):
msgs = await self.redis.lrange(f"chat:{session_id}", 0, -1)
summary = await self.llm.summarize(msgs, max_tokens=300)
await self.redis.delete(f"chat:{session_id}")
await self.redis.lpush(f"chat:{session_id}",
json.dumps({"role": "system", "content": f"[摘要] {summary}"}))
追问应对:
- Q:摘要会丢关键信息吗?
会,但可控。我们让 LLM 摘要时强制保留"用户明确意图 / 已完成步骤 / 关键数值 / 未决待办"四类,并保留最近 3 轮原文不摘要,敏感场景用 RAG 把历史向量化按需检索补全。
二、符号化建模 DSL(亮点项目)
Q5:为什么要做符号化建模 DSL?解决了什么问题?
标准答案:
问题背景:能源场景的"负荷计算 / 能流分析"要求数值严格可复算,纯 LLM 输出数字幻觉率高达 30%,且结果不可审计。
解决方案 — 职责分离:
- LLM 只负责把自然语言 → 符号表达式(如
A → (B ⊕ C) → D); - 确定性引擎拿到符号表达式做严格计算(拓扑解析 + 端口校验);
- 符号表达式同步渲染成可视化工程图,用户能直接看/改;
- 幻觉率从 30% → 5%,且每步可解释。
核心代码骨架(DSL 解析 + 求解):
from dataclasses import dataclass
from typing import List
@dataclass
class Node:
name: str
inputs: List[str]
outputs: List[str]
class DSLParser:
"""A → (B ⊕ C) → D 这种字符串转执行图"""
def parse(self, expr: str) -> dict:
tokens = self._tokenize(expr)
ast = self._build_ast(tokens)
graph = self._ast_to_graph(ast)
self._validate_ports(graph) # 端口类型/方向校验
self._check_cycle(graph) # 循环依赖检测
return graph
class DeterministicSolver:
def __init__(self, node_registry):
self.registry = node_registry # name -> 计算函数
def solve(self, graph: dict, inputs: dict) -> dict:
results = {}
for node in self._topo_sort(graph):
fn = self.registry[node.name]
args = {k: results.get(k, inputs.get(k)) for k in node.inputs}
results[node.name] = fn(**args)
return results
# LLM 端只产 DSL,不算数字
async def llm_to_dsl(query: str, llm) -> str:
prompt = f"""把任务转成符号表达式,仅用以下算子: → ⊕ ∥ ()
可用节点: {list(node_registry.keys())}
任务: {query}
只输出表达式,不要解释。"""
return await llm.complete(prompt, temperature=0)
追问应对:
- Q:5% 的剩余幻觉在哪?
主要在 LLM 错误编排——DSL 词表没覆盖到的边缘场景。靠两条路压:① 扩词表;② 给 LLM 加 few-shot 触发新词汇时让它"先问后做",模糊时反问用户而不是猜。
Q6:DSL 怎么设计的?为什么用这种语法?
标准答案:
- 基本元素:节点(A、B、C)+ 连接符(
→串行、⊕合并、∥并行); - 括号嵌套:表示子流程优先级;
- 类型系统:节点有 input/output port 类型,引擎在解析阶段做兼容性校验;
- 设计考量:① LLM 容易生成(语法简单、token 少);② 人能读懂(直观);③ 能无歧义映射到执行图。
核心代码骨架(端口校验):
@dataclass
class Port:
name: str
dtype: str # "energy" / "signal" / "data"
direction: str # "in" / "out"
class PortValidator:
def validate_edge(self, from_node: Node, to_node: Node):
# 找上游 out 口 + 下游 in 口
out_ports = [p for p in from_node.ports if p.direction == "out"]
in_ports = [p for p in to_node.ports if p.direction == "in"]
# 至少有一对类型兼容
for op in out_ports:
for ip in in_ports:
if self._compatible(op.dtype, ip.dtype):
return op, ip
raise ValueError(f"端口类型不兼容: {from_node.name} → {to_node.name}")
def _compatible(self, a, b):
# 类型层级:energy 可降级到 signal,反之不行
hierarchy = {"energy": 2, "signal": 1, "data": 0}
return hierarchy.get(a, -1) >= hierarchy.get(b, -1)
追问应对:
- Q:为啥不直接用 Python 代码当 DSL?
Python 表达力太强,LLM 容易"自由发挥"写出无法静态校验的代码。我们的 DSL 是受限语言,可枚举、可校验、可可视化——本质是"为 LLM 设计的安全 sandbox"。
Q7:怎么验证 DSL 生成的正确性?
标准答案:
四层验证:
- 语法层:解析器拒绝非法 token;
- 结构层:端口类型校验、循环依赖检测、孤立节点检测;
- 业务层:领域硬约束(能量守恒、单位一致);
- 回归层:标准用例集,每次 prompt/模型变更回归。
核心代码骨架(回归测试):
import pytest
REGRESSION_CASES = [
("计算 A 设备到 D 的能流", "A → B → D"),
("B 和 C 并联后接 D", "(B ∥ C) → D"),
]
@pytest.mark.parametrize("query,expected_dsl", REGRESSION_CASES)
async def test_dsl_generation(query, expected_dsl):
actual = await llm_to_dsl(query, llm)
# 不强求字符串相等,转 AST 后做结构等价
assert ast_equal(parser.parse(actual), parser.parse(expected_dsl))
async def test_energy_conservation():
graph = parser.parse("A(100kW) → B → C")
result = solver.solve(graph, {"A": 100})
total_in = sum(result[n] for n in graph.inputs)
total_out = sum(result[n] for n in graph.outputs)
assert abs(total_in - total_out) < 0.01 # 能量守恒
追问应对:
- Q:业务约束怎么扩展?
写成插件——每个约束实现
Constraint.check(graph, result) -> bool,注册到 ConstraintRegistry,求解后逐个跑。新加约束零侵入。
三、声明式任务路由(亮点项目)
Q8:GMM + BIC 自动选 K 聚类用来做什么?
标准答案:
场景:任务描述输入后要自动分类到合适的任务类型(查询/控制/分析/告警),但任务类别数 K 先验未知。
做法:
- jieba 分词 + 向量化(TF-IDF / Embedding);
- GMM(高斯混合)聚类,候选 K = 2~10;
- BIC(贝叶斯信息准则)选最优 K——平衡拟合度和模型复杂度;
- 新任务进来 → 向量化 → 算到各簇中心距离 → 落到最近簇 → 匹配该簇绑定的工具链。
核心代码骨架:
import numpy as np
from sklearn.mixture import GaussianMixture
from sklearn.feature_extraction.text import TfidfVectorizer
import jieba
class TaskClusterer:
def __init__(self):
self.vec = TfidfVectorizer(tokenizer=jieba.lcut)
self.gmm = None
self.cluster_to_toolchain = {}
def fit(self, task_descriptions: list[str], k_range=(2, 11)):
X = self.vec.fit_transform(task_descriptions).toarray()
best_bic, best_gmm = float("inf"), None
for k in range(*k_range):
gmm = GaussianMixture(n_components=k, random_state=42).fit(X)
bic = gmm.bic(X) # BIC 越小越好
if bic < best_bic:
best_bic, best_gmm = bic, gmm
self.gmm = best_gmm
def route(self, task_desc: str) -> int:
X = self.vec.transform([task_desc]).toarray()
cluster_id = self.gmm.predict(X)[0]
return self.cluster_to_toolchain[cluster_id]
追问应对:
-
Q:为啥不直接让 LLM 分类?
LLM 分类成本高、慢、不稳定。GMM 是离线训好的轻量模型,毫秒级路由,对高频简单任务用它能省 80% LLM 调用。LLM 留给真正需要推理的任务。
-
Q:新任务类型出现怎么办?
每天离线 retrain 一次 GMM;线上同时记录"低置信度路由"(落到簇但距离阈值外),周维度人工 review 是否要新增工具链。
Q9:声明式配置 + LLM 编排 + 三层回退链是什么?
标准答案:
- 声明式配置:任务用 YAML 声明所需能力/工具/资源,不写过程代码;
- LLM 编排:运行时由 LLM 根据声明 + 上下文决定调用顺序和参数;
- 三层回退:
- 首选路径:标准工具链;
- 降级路径:核心工具不可用时走简化版;
- 兜底路径:自动化失败时进人工标注队列。
- 新任务只改 YAML 不改主流程代码。
核心代码骨架:
# tasks/energy_check.yaml
name: energy_check
description: 检查指定设备能耗是否异常
capabilities:
- read_device_metrics
- compare_threshold
fallback:
primary: [query_realtime, ml_anomaly_detect, notify]
degraded: [query_historical, rule_based_detect, notify]
manual: [create_ticket]
class FallbackExecutor:
async def run(self, task_config: dict, query: str):
for level in ["primary", "degraded", "manual"]:
chain = task_config["fallback"][level]
try:
# LLM 根据 query 和工具链规划参数
plan = await self.llm.plan_with_chain(query, chain)
return await self._run_chain(plan)
except (ToolUnavailable, TimeoutError) as e:
log.warning(f"{level} failed: {e}, fallback to next")
continue
raise RuntimeError("all levels failed")
async def _run_chain(self, plan):
result = None
for step in plan:
result = await registry.invoke(step["tool"], step["args"])
if result["status"] == "error":
raise ToolUnavailable(result["error"])
return result
追问应对:
- Q:降级路径效果差很多怎么办?
降级路径定位是"可用 > 完美",业务方接受度通过产品侧表达管理——返回结果带"降级模式"标签,告知用户精度可能下降。同时监控降级触发率,超过 5% 就排查首选路径稳定性。
四、通用 AI 工程问题
Q10:怎么评估一个 Agent 系统的质量?
标准答案:
| 维度 | 指标 | 采集方式 |
|---|---|---|
| 任务完成率 | success rate | 离线集 + 人工标注 |
| 工具调用准确率 | tool call accuracy | 链路日志统计 |
| 幻觉率 | hallucination rate | 业务规则检测 + 抽样人审 |
| 延迟 | P50/P95 latency | Kafka trace |
| 成本 | token / request | 计费日志 |
| 用户满意度 | thumbs up/down | 前端埋点 |
核心代码骨架(离线评测):
class EvalDataset:
def __init__(self, cases: list[dict]):
self.cases = cases # [{"query": "...", "expected": "...", "rubric": "..."}]
class AgentEvaluator:
async def run(self, agent, dataset, judge_llm):
results = []
for case in dataset.cases:
actual = await agent.run(case["query"], session_id="eval")
verdict = await judge_llm.judge(
f"问题: {case['query']}\n期望: {case['expected']}\n实际: {actual}\n规则: {case['rubric']}\n输出 JSON: {{score: 0-5, reason: str}}"
)
results.append({**case, "actual": actual, **verdict})
return self._aggregate(results)
def _aggregate(self, results):
scores = [r["score"] for r in results]
return {
"success_rate": sum(1 for s in scores if s >= 4) / len(scores),
"avg_score": sum(scores) / len(scores),
"p95_score": np.percentile(scores, 95),
}
追问应对:
- Q:LLM as Judge 不偏吗?
会偏。三招对冲:① 用比被测模型更强的模型当 judge(如被测是 Sonnet,judge 用 Opus);② 多 judge 投票;③ 抽 10% 人工 review 校准 judge 准确率。
Q11:怎么控制 LLM 调用成本?
标准答案:
6 大手段,按 ROI 排:
- 缓存:query hash 命中直返(DeepPark 命中率 ~40%);
- Prompt Cache:长 system prompt 用厂商 prompt cache(Anthropic / OpenAI 都支持),命中部分按 10% 计费;
- 分级路由:简单意图小模型,复杂推理大模型;
- 流式:边生成边消费,用户感知延迟低,允许用更长 prompt;
- 批处理:非实时任务走 batch API(半价);
- 并发限制:信号量限并发,避免突发被限流。
核心代码骨架(带缓存的 LLM 客户端):
import hashlib
from anthropic import AsyncAnthropic
class CachedLLM:
def __init__(self, redis, ttl=3600):
self.client = AsyncAnthropic()
self.redis = redis
self.ttl = ttl
self.sem = asyncio.Semaphore(20)
async def chat(self, system: str, messages: list, model="claude-sonnet-4-6"):
cache_key = self._key(system, messages, model)
cached = await self.redis.get(cache_key)
if cached:
return cached.decode()
async with self.sem:
resp = await self.client.messages.create(
model=model,
max_tokens=1024,
system=[{"type": "text", "text": system,
"cache_control": {"type": "ephemeral"}}], # prompt cache
messages=messages,
)
text = resp.content[0].text
await self.redis.setex(cache_key, self.ttl, text)
return text
def _key(self, system, messages, model):
raw = f"{model}|{system}|{json.dumps(messages, ensure_ascii=False)}"
return f"llm:{hashlib.sha256(raw.encode()).hexdigest()}"
追问应对:
- Q:缓存命中率怎么提?
三招:① 把不变的 system prompt 和动态 user message 拆开,system 部分用 prompt cache(厂商侧);② 业务侧对常见 query 做"标准化"(去标点 / 大小写归一)增加命中;③ 半结构化任务把固定模板和变量分离,模板部分缓存。
Q12:Function Calling 与 MCP 协议的区别?
标准答案:
- Function Calling:模型厂商私有协议(OpenAI 提出,各家跟进),定义工具描述格式和调用结构,绑定具体模型;
- MCP(Model Context Protocol):Anthropic 提出的跨模型工具协议,把工具/资源/Prompt 抽象成 server,任何支持 MCP 的客户端都能用,类似"AI 界的 LSP";
- 实践:内部统一用 MCP server 实现工具,外部对接不同模型时按各家 Function Calling 格式做 adapter 转换。
核心代码骨架(同一工具两种暴露方式):
# 内部 MCP server(跨客户端通用)
from mcp.server import Server
from mcp.types import Tool
server = Server("energy-tools")
@server.list_tools()
async def list_tools():
return [Tool(name="query_device",
description="按 ID 查设备",
inputSchema={"type": "object",
"properties": {"device_id": {"type": "string"}},
"required": ["device_id"]})]
@server.call_tool()
async def call_tool(name, args):
if name == "query_device":
return await db.fetch_one("SELECT * FROM device WHERE id=$1", args["device_id"])
# 外部对接 Anthropic SDK(Function Calling 格式)
def mcp_tool_to_anthropic(tool: Tool) -> dict:
return {"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema}
追问应对:
- Q:MCP 生产就绪了吗?
2025 年起 Claude Code / Cursor / Cline 等主流客户端都原生支持。我们内部新工具优先 MCP 实现,老代码逐步迁移。对外服务时仍要保留 Function Calling adapter 兼容 OpenAI / 国产模型。
Q13:Prompt 工程你常用哪些技巧?
标准答案:
- 四段式结构:角色 + 任务 + 约束 + 输出格式;
- Few-shot:2~3 个高质量示例,质量 > 数量;
- CoT:让模型先输出 reasoning 再给结论,复杂推理必备;
- 结构化输出:JSON Schema / Pydantic 约束,下游可解析;
- Prompt 模板化:Jinja 管理变量,方便 A/B;
- 缓存:长 system prompt 用 prompt cache。
核心代码骨架(带 Pydantic 校验的结构化输出):
from pydantic import BaseModel, Field
import instructor # 自动重试到 Schema 通过
class DeviceQuery(BaseModel):
device_id: str = Field(..., description="设备唯一ID")
metric: str = Field(..., description="指标名,如 power/voltage")
time_range_hours: int = Field(default=24, ge=1, le=720)
client = instructor.from_anthropic(AsyncAnthropic())
async def parse_query(natural_query: str) -> DeviceQuery:
return await client.messages.create(
model="claude-sonnet-4-6",
max_tokens=300,
response_model=DeviceQuery,
messages=[{
"role": "user",
"content": f"把下面这句话转成结构化查询:\n{natural_query}"
}],
)
追问应对:
- Q:Few-shot 选哪些例子最有效?
选最容易被模型搞错的边缘案例——不是典型例子,而是"看似简单但模型常翻车"的。定期从线上 bad case 集里挑,比脑补的例子有效得多。
Q14:怎么处理 LLM 的不确定性?
标准答案:
- 温度控制:确定性任务 temperature=0,创造性任务 0.7+;
- Self-consistency:同问题采样多次取多数;
- 多模型投票:Kimi + GPT + Claude 对比;
- 规则后处理:业务硬约束用代码兜底;
- 置信度评估:让模型自评信心,低信心走人工。
核心代码骨架(self-consistency 投票):
from collections import Counter
async def vote_answer(query: str, llm, n=5):
tasks = [llm.chat(system="...", messages=[{"role": "user", "content": query}],
temperature=0.7) for _ in range(n)]
answers = await asyncio.gather(*tasks)
# 归一化(去空格 / 小写)后投票
normalized = [a.strip().lower() for a in answers]
counter = Counter(normalized)
top, count = counter.most_common(1)[0]
confidence = count / n
if confidence < 0.6:
return {"answer": None, "confidence": confidence, "need_human": True}
return {"answer": top, "confidence": confidence, "need_human": False}
追问应对:
- Q:5 次采样成本太高?
分场景。关键决策(数值计算、操作下发)值得;闲聊不需要。我们的策略:先 1 次跑,若模型自评信心 < 0.7 再触发投票,整体成本上升约 15%。
五、可能被追问的"坑"
-
Q:你的幻觉率从 30%→5% 是怎么量的?
500 条离线测试集覆盖典型查询/计算/控制场景。30% 是上线初期一周抽样的人工评估,5% 是符号化方案上线后同样测试集 + 同样口径的结果。剩余 5% 主要在 LLM 错误编排(DSL 没覆盖的边缘),后续靠扩 DSL 词表继续降。
-
Q:Plan-Execute 重规划成本高怎么办?
Plan 阶段就让 LLM 输出检查点(每步预期结果),执行阶段不匹配先 ReAct 局部修正,修正失败才全局 Replan。这样 80% 的扰动靠局部修复,全局 Replan 触发率 < 5%。
-
Q:Kafka 链路追踪具体追什么?
每个 Agent 步骤打 trace:input / output / 工具调用 / token / 耗时 / 错误码。下游 Flink 实时聚合做告警,离线 Spark 挖异常 pattern(如"哪些 prompt 容易触发工具调用错误")。