AI / Agent 工程面试题(完整答案版)

7 阅读7分钟

锚点项目:浙大 X-Lab 能源 Agent Harness、符号化建模 DSL、声明式任务路由 每题包含:问题 → 标准答案 → 核心代码 → 追问应对


一、Agent 架构基础

Q1:讲一下你设计的 Agent Harness 整体架构

标准答案

整套 Harness 分为 6 层,自顶向下:

  1. 入口层:自然语言指令进入 → 意图分类 + 任务模板匹配,决定走哪条主路径;
  2. 规划层Plan-Execute + ReAct 混合——简单任务走 ReAct(边想边做),复杂多步任务走 Plan-Execute(先全局规划,再逐步执行),中间设检查点检测偏离;
  3. 执行层:有限状态机驱动,每步可在执行中动态推理调整下一步;
  4. 工具层:Function Calling + JSON Schema 参数校验 + 工具结果按统一结构回注;
  5. 记忆层短/中/长三层记忆 + 上下文压缩 + token 配额;
  6. 观测层: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 有什么区别?为什么要混合?

标准答案

维度ReActPlan-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 生成的正确性?

标准答案

四层验证:

  1. 语法层:解析器拒绝非法 token;
  2. 结构层:端口类型校验、循环依赖检测、孤立节点检测;
  3. 业务层:领域硬约束(能量守恒、单位一致);
  4. 回归层:标准用例集,每次 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 根据声明 + 上下文决定调用顺序和参数;
  • 三层回退
    1. 首选路径:标准工具链;
    2. 降级路径:核心工具不可用时走简化版;
    3. 兜底路径:自动化失败时进人工标注队列。
  • 新任务只改 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 latencyKafka 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 排:

  1. 缓存:query hash 命中直返(DeepPark 命中率 ~40%);
  2. Prompt Cache:长 system prompt 用厂商 prompt cache(Anthropic / OpenAI 都支持),命中部分按 10% 计费;
  3. 分级路由:简单意图小模型,复杂推理大模型;
  4. 流式:边生成边消费,用户感知延迟低,允许用更长 prompt;
  5. 批处理:非实时任务走 batch API(半价);
  6. 并发限制:信号量限并发,避免突发被限流。

核心代码骨架(带缓存的 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 容易触发工具调用错误")。