Skip to content

08 工程化实践(面试八股文) ​

面向零基础读者的系统梳理:把 AI Agent 从「能跑」做成「可观测、可容错、可计费、可审计、可上线」。每个知识点均包含「概念 + 原理 + 面试问答 + 追问 + 代码(如适用)」,建议配合一个小型生产级 Demo(路由 + 熔断 + 日志 + 评估集)动手练习。

目录 ​

  1. 模型路由与容错
  2. Token 成本控制
  3. 全链路可观测性
  4. 安全与权限
  5. 部署与运维
  6. 性能优化
  7. 评估与测试
  8. 幻觉问题的工程解决方案
  9. 综合面试题库(15+ 题)

1. 模型路由与容错 ​

1.1 概念解释(通俗易懂) ​

模型路由(Model Routing):像快递分拣中心——根据「任务类型、成本预算、延迟要求、合规区域」决定这一请求该走哪家模型,而不是所有流量都打到同一个端点。

容错(Fault Tolerance):当某家模型超时、限流、报错、质量异常时,系统仍能自动切换、重试或降级,对用户表现为「慢一点」或「差一点但可用」,而不是整站不可用。

多模型混用:OpenAI、Anthropic Claude、国产模型(通义、文心、智谱等)往往接口相似但细节不同(消息格式、工具协议、最大上下文、计费方式)。工程上需要统一抽象层,避免业务代码散落 if vendor == ...。

1.2 原理详解 ​

1.2.1 多模型管理(统一抽象) ​

典型做法:

  1. Provider Adapter:每个厂商一个适配器,实现统一接口,例如 chat(messages, tools, model_id) -> Completion。
  2. 配置中心:维护 model_id → endpoint、密钥、RPM/TPM 限额、区域、价格表。
  3. 能力矩阵:标注「是否支持 JSON Mode / 函数调用 / 视觉 / 长上下文」,路由时做能力匹配。
  4. 密钥与合规:国产与海外模型可能涉及数据出境与行业合规,路由不仅是性能问题,也是合规路由。

1.2.2 优先级调度策略 ​

常见策略(可组合):

策略含义适用
固定优先级列表顺序尝试简单可靠
成本优先在满足质量阈值下选最便宜批处理、非实时
延迟优先SLA 内选最快交互式产品
负载感知结合队列深度、429 率动态调整大规模生产
任务分类路由代码类→A,摘要类→B提高性价比

注意:优先级不是「永远用大模型」,而是在约束下最优化(成本、延迟、质量、可用性)。

1.2.3 三态熔断器(Circuit Breaker) ​

类比电路保险丝:失败太多就暂时断开,避免把下游打挂,同时给下游恢复时间。

三态:

  1. Closed(闭合):正常转发请求;统计失败率/连续失败次数。
  2. Open(打开):失败超阈值,快速失败(不再调用下游),进入冷却。
  3. Half-Open(半开):冷却结束,放行少量探测请求;成功则回到 Closed,失败则回到 Open。

与重试的关系:熔断解决「系统性故障时的雪崩」;重试解决「偶发网络抖动」。二者常一起用:熔断打开时不再重试下游,而是走降级。

1.2.4 自动降级(例:GPT-4 → GPT-3.5) ​

触发条件示例:超时、5xx、429、熔断 Open、或「质量评分低于阈值且允许降级」。

降级策略:

  • 模型降级:强模型 → 弱模型(成本与延迟下降,能力可能下降)。
  • 功能降级:关掉「多步 Agent」,改为「单轮问答」;或减少工具数量。
  • 输出降级:从「长文」改为「要点列表」。

关键:降级要可观测(日志标明 degraded=true),并最好对用户透明提示(视产品策略)。

1.2.5 重试与指数退避(Exponential Backoff) ​

为何指数退避:429/503 往往是「瞬时过载」,立刻重试会加剧拥堵(惊群效应)。退避让请求在时间轴上摊开。

常见公式:wait = min(cap, base * 2^attempt + jitter),其中 jitter 为随机抖动,避免同一时刻大量客户端同时重试。

可重试 vs 不可重试:

  • 可重试:超时、连接错误、429、部分 5xx。
  • 不可重试:401/403、400(参数错误)、内容政策拒绝等——应直接失败或换策略。

1.3 面试问题(Q)与标准答案(A) ​

  • Q1:为什么要做模型路由,而不是全量用一个最强模型?
    • A: 最强模型往往更贵、更慢,且并非所有子任务都需要。路由能在质量、成本、延迟、可用性、合规之间做权衡;同时多供应商提高容灾能力,避免单点故障。
  • Q2:熔断器和重试分别解决什么问题?一起用时要注意什么?
    • A: 熔断防止故障扩散和雪崩;重试提高偶发失败的成功率。一起用时应在熔断 Open 阶段停止对同一下游的盲目重试,改为降级或切换供应商;并对 429 使用限流 + 退避,避免放大拥堵。

1.4 可能的追问及应对 ​

  • 追问:Half-Open 放多少流量探测合适?
    • 应对: 用令牌桶或固定并发 1~N 条探测,观察错误率;也可结合滑动窗口统计半开阶段成功率,避免一开就灌满。
  • 追问:降级后如何保证体验?
    • 应对: 产品侧提示、缩短输出、启用缓存结果;技术上对关键路径保留同步强模型,非关键用弱模型。

1.5 代码示例:Python 实现简易熔断器 + 指数退避 ​

python
import random
import time
from dataclasses import dataclass
from enum import Enum, auto


class State(Enum):
    CLOSED = auto()
    OPEN = auto()
    HALF_OPEN = auto()


@dataclass
class CircuitBreaker:
    failure_threshold: int = 5
    success_threshold: int = 2  # 半开阶段连续成功次数
    open_seconds: float = 30.0
    half_open_max_calls: int = 3

    def __post_init__(self):
        self.state = State.CLOSED
        self.failures = 0
        self.successes_half = 0
        self.open_until = 0.0
        self.half_open_inflight = 0

    def _trip(self):
        self.state = State.OPEN
        self.open_until = time.time() + self.open_seconds
        self.failures = 0
        self.successes_half = 0
        self.half_open_inflight = 0

    def allow(self) -> bool:
        now = time.time()
        if self.state == State.OPEN:
            if now >= self.open_until:
                self.state = State.HALF_OPEN
                self.successes_half = 0
                self.half_open_inflight = 0
            else:
                return False
        if self.state == State.HALF_OPEN:
            return self.half_open_inflight < self.half_open_max_calls
        return True

    def before_call(self):
        if self.state == State.HALF_OPEN:
            self.half_open_inflight += 1

    def on_success(self):
        if self.state == State.HALF_OPEN:
            self.successes_half += 1
            if self.successes_half >= self.success_threshold:
                self.state = State.CLOSED
                self.successes_half = 0
        else:
            self.failures = 0
        if self.state == State.HALF_OPEN:
            self.half_open_inflight = max(0, self.half_open_inflight - 1)

    def on_failure(self):
        self.failures += 1
        if self.state == State.HALF_OPEN or self.failures >= self.failure_threshold:
            self._trip()
        if self.state == State.HALF_OPEN:
            self.half_open_inflight = max(0, self.half_open_inflight - 1)


def exponential_backoff(attempt: int, base: float = 0.5, cap: float = 8.0) -> float:
    jitter = random.random() * 0.25
    return min(cap, base * (2**attempt) + jitter)


def call_with_breaker_and_retry(fn, breaker: CircuitBreaker, max_retries: int = 3):
    for attempt in range(max_retries):
        if not breaker.allow():
            raise RuntimeError("circuit_open")
        breaker.before_call()
        try:
            result = fn()
            breaker.on_success()
            return result
        except Exception:
            breaker.on_failure()
            if attempt == max_retries - 1:
                raise
            time.sleep(exponential_backoff(attempt))
import random
import time
from dataclasses import dataclass
from enum import Enum, auto


class State(Enum):
    CLOSED = auto()
    OPEN = auto()
    HALF_OPEN = auto()


@dataclass
class CircuitBreaker:
    failure_threshold: int = 5
    success_threshold: int = 2  # 半开阶段连续成功次数
    open_seconds: float = 30.0
    half_open_max_calls: int = 3

    def __post_init__(self):
        self.state = State.CLOSED
        self.failures = 0
        self.successes_half = 0
        self.open_until = 0.0
        self.half_open_inflight = 0

    def _trip(self):
        self.state = State.OPEN
        self.open_until = time.time() + self.open_seconds
        self.failures = 0
        self.successes_half = 0
        self.half_open_inflight = 0

    def allow(self) -> bool:
        now = time.time()
        if self.state == State.OPEN:
            if now >= self.open_until:
                self.state = State.HALF_OPEN
                self.successes_half = 0
                self.half_open_inflight = 0
            else:
                return False
        if self.state == State.HALF_OPEN:
            return self.half_open_inflight < self.half_open_max_calls
        return True

    def before_call(self):
        if self.state == State.HALF_OPEN:
            self.half_open_inflight += 1

    def on_success(self):
        if self.state == State.HALF_OPEN:
            self.successes_half += 1
            if self.successes_half >= self.success_threshold:
                self.state = State.CLOSED
                self.successes_half = 0
        else:
            self.failures = 0
        if self.state == State.HALF_OPEN:
            self.half_open_inflight = max(0, self.half_open_inflight - 1)

    def on_failure(self):
        self.failures += 1
        if self.state == State.HALF_OPEN or self.failures >= self.failure_threshold:
            self._trip()
        if self.state == State.HALF_OPEN:
            self.half_open_inflight = max(0, self.half_open_inflight - 1)


def exponential_backoff(attempt: int, base: float = 0.5, cap: float = 8.0) -> float:
    jitter = random.random() * 0.25
    return min(cap, base * (2**attempt) + jitter)


def call_with_breaker_and_retry(fn, breaker: CircuitBreaker, max_retries: int = 3):
    for attempt in range(max_retries):
        if not breaker.allow():
            raise RuntimeError("circuit_open")
        breaker.before_call()
        try:
            result = fn()
            breaker.on_success()
            return result
        except Exception:
            breaker.on_failure()
            if attempt == max_retries - 1:
                raise
            time.sleep(exponential_backoff(attempt))

2. Token 成本控制 ​

2.1 概念解释 ​

大模型计费通常与 Token(词元)强相关。Agent 又多轮、多工具,上下文膨胀极快。成本控制的目标是:在可接受质量下,让每次会话、每个任务、每个租户的支出可预测、可优化、可告警。

2.2 原理详解 ​

2.2.1 Token 计数方法(tiktoken 等) ​

tiktoken:OpenAI 官方常用分词库,按 encoding_for_model(model_name) 选择编码,用 encode(text) 得到 token 列表与数量。

注意:不同模型编码不同;计费以服务商账单为准,本地计数是估算,用于预算与截断策略。

实践:在网关层记录 prompt_tokens、completion_tokens;与 API 返回值对账以校准估算误差。

2.2.2 Prompt 精简技巧 ​

  • 删除冗余示例与重复指令;合并系统提示为一版权威描述。
  • 使用结构化输出(JSON)减少来回澄清。
  • 对长文档:摘要后再喂模型,或 RAG 只取 Top-K 片段。
  • 消息裁剪:保留 system + 最近 N 轮 + 关键摘要(见记忆模块)。

2.2.3 缓存策略(含语义缓存) ​

类型做法优点注意
精确缓存请求哈希完全一致命中实现简单命中率低
语义缓存embedding 相似度高于阈值则复用命中率高需防「相似但意图不同」
结果分层热问题走缓存,冷问题走模型省成本需 TTL 与失效

语义缓存风险:用户问题措辞不同但意图相同——可用;若敏感场景(医疗法律)语义相近但条件不同,复用可能出错,需阈值 + 策略类路由。

2.2.4 模型选择优化与批量处理 ​

  • 小模型做分类/路由/摘要,大模型做最终生成。
  • 批量(Batch)API(若云厂商提供):非实时场景单价更低。
  • 合并请求:多个短任务合成一次调用(注意上下文隔离与提示设计)。

2.2.5 成本监控与告警 ​

  • 维度:租户 / 功能 / 模型 / Agent 步骤。
  • 指标:日均 Token、单次 P95 成本、异常飙升、某 Prompt 模板成本占比。
  • 告警:环比、阈值、预算封顶(硬限制返回友好错误)。

2.3 面试问题(Q)与标准答案(A) ​

  • Q3:为什么说本地 tiktoken 计数只能「估算」?
    • A: 实际计费依赖服务商的分词器版本、特殊 token、多模态输入等;不同模型与版本可能不一致。本地计数用于预算控制与截断,最终应以 API 返回的 usage 与账单对账。
  • Q4:语义缓存和精确缓存怎么选?
    • A: 低成本、高 QPS 的重复咨询场景适合语义缓存;对强合规与强正确性场景要谨慎,需提高阈值、加业务校验或禁用缓存。精确缓存适合完全幂等的调用(如同参数批处理)。

2.4 可能的追问及应对 ​

  • 追问:如何防止缓存「串味」?
    • 应对: 缓存键包含租户 ID、模型版本、Prompt 版本、工具集版本;语义缓存加意图分类再匹配。

2.5 代码示例:tiktoken 计数 + 简单请求哈希缓存 ​

python
import hashlib
import json
from functools import lru_cache

try:
    import tiktoken
except ImportError:
    tiktoken = None


def approx_token_count(text: str, model: str = "gpt-4o") -> int:
    if tiktoken is None:
        return max(1, len(text) // 4)
    enc = tiktoken.encoding_for_model(model)
    return len(enc.encode(text))


def request_fingerprint(system: str, user: str, model: str) -> str:
    raw = json.dumps({"system": system, "user": user, "model": model}, sort_keys=True)
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()


# 生产环境请用 Redis;此处演示 LRU
@lru_cache(maxsize=1024)
def cached_exact(system: str, user: str, model: str) -> str | None:
    return None  # 占位:实际应 get from Redis


def set_exact_cache(system: str, user: str, model: str, response: str) -> None:
    key = request_fingerprint(system, user, model)
    # redis.setex(key, ttl, response)
    cached_exact.cache_clear()  # 演示用,勿在生产使用
import hashlib
import json
from functools import lru_cache

try:
    import tiktoken
except ImportError:
    tiktoken = None


def approx_token_count(text: str, model: str = "gpt-4o") -> int:
    if tiktoken is None:
        return max(1, len(text) // 4)
    enc = tiktoken.encoding_for_model(model)
    return len(enc.encode(text))


def request_fingerprint(system: str, user: str, model: str) -> str:
    raw = json.dumps({"system": system, "user": user, "model": model}, sort_keys=True)
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()


# 生产环境请用 Redis;此处演示 LRU
@lru_cache(maxsize=1024)
def cached_exact(system: str, user: str, model: str) -> str | None:
    return None  # 占位:实际应 get from Redis


def set_exact_cache(system: str, user: str, model: str, response: str) -> None:
    key = request_fingerprint(system, user, model)
    # redis.setex(key, ttl, response)
    cached_exact.cache_clear()  # 演示用,勿在生产使用

3. 全链路可观测性 ​

3.1 概念解释 ​

可观测性(Observability):不仅知道「错了」,还能从日志、指标、链路还原「错在哪一步、哪个工具、哪个模型、哪个租户」。对 Agent 尤其重要,因为路径是动态多步的,没有 Trace 很难排障。

3.2 原理详解 ​

3.2.1 日志系统(结构化日志) ​

使用 JSON 一行一条(或等价结构化字段),便于 ELK、Loki、ClickHouse 查询。

必备字段示例:timestamp、level、trace_id、span_id、tenant_id、agent_name、step、model、latency_ms、token_usage、error_code。

切忌:只打一大段自然语言,没有可过滤字段。

3.2.2 链路追踪(Trace) ​

  • Trace:一次用户请求从头到尾。
  • Span:其中一个单元(一次 LLM 调用、一次工具执行、一次检索)。
  • 父子关系:span_id / parent_span_id 构成树,还原 Agent 的思考链路与并行分支。

3.2.3 每个 Agent 步骤的记录 ​

建议为每步记录:step_index、thought 摘要、tool_name、tool_args(脱敏后)、observation 摘要、耗时、重试次数、是否降级。

3.2.4 LangSmith / LangFuse 等工具 ​

  • LangSmith:与 LangChain 生态结合紧,便于调试链路与数据集。
  • LangFuse:开源,可自托管,侧重产品分析 + 成本 + 质量。

价值:统一看 Trace、对比 Prompt 版本、关联评估集。

3.2.5 自定义 Trace 实现思路 ​

最小实现:contextvars 存 trace_id,with span("llm") 记录开始结束;导出到 OpenTelemetry,或写入 Kafka 再由 Flink 聚合。

3.3 面试问题(Q)与标准答案(A) ​

  • Q5:Agent 系统为什么比传统服务更需要 Trace?
    • A: 传统服务调用链相对固定;Agent 路径依赖模型决策,分支多、偶现问题多。Trace 能把「哪一步选了哪个工具、参数是什么、返回多长」串起来,否则只能猜 Prompt。
  • Q6:结构化日志和 Trace 有什么区别?
    • A: 结构化日志是事件流,适合检索与告警;Trace 是因果树,适合分析延迟与依赖。二者应通过 trace_id 关联,互补而非二选一。

3.4 可能的追问及应对 ​

  • 追问:日志里能记完整 Prompt 吗?
    • 应对: 开发环境可记;生产应脱敏 + 采样 + 权限,避免 PII 与密钥泄露。

3.5 代码示例:简易 Span + 结构化日志 ​

python
import json
import time
import uuid
from contextvars import ContextVar

trace_var: ContextVar[str | None] = ContextVar("trace_id", default=None)


def new_trace() -> str:
    tid = str(uuid.uuid4())
    trace_var.set(tid)
    return tid


def log_event(level: str, msg: str, **fields):
    payload = {
        "level": level,
        "msg": msg,
        "trace_id": trace_var.get(),
        **fields,
    }
    print(json.dumps(payload, ensure_ascii=False))


class Span:
    def __init__(self, name: str):
        self.name = name

    def __enter__(self):
        self.start = time.perf_counter()
        log_event("INFO", "span_start", span=self.name)
        return self

    def __exit__(self, exc_type, exc, tb):
        ms = (time.perf_counter() - self.start) * 1000
        log_event("INFO", "span_end", span=self.name, latency_ms=round(ms, 2), error=bool(exc))
        return False


# 用法:new_trace(); with Span("llm_call"): ...
import json
import time
import uuid
from contextvars import ContextVar

trace_var: ContextVar[str | None] = ContextVar("trace_id", default=None)


def new_trace() -> str:
    tid = str(uuid.uuid4())
    trace_var.set(tid)
    return tid


def log_event(level: str, msg: str, **fields):
    payload = {
        "level": level,
        "msg": msg,
        "trace_id": trace_var.get(),
        **fields,
    }
    print(json.dumps(payload, ensure_ascii=False))


class Span:
    def __init__(self, name: str):
        self.name = name

    def __enter__(self):
        self.start = time.perf_counter()
        log_event("INFO", "span_start", span=self.name)
        return self

    def __exit__(self, exc_type, exc, tb):
        ms = (time.perf_counter() - self.start) * 1000
        log_event("INFO", "span_end", span=self.name, latency_ms=round(ms, 2), error=bool(exc))
        return False


# 用法:new_trace(); with Span("llm_call"): ...

4. 安全与权限 ​

4.1 概念解释 ​

Agent 能读数据、调工具、执行动作,攻击面大于普通聊天。工程上要把「模型不可信」作为默认假设:输入可能是恶意的,输出可能是有害的,工具调用必须经过权限与校验。

4.2 原理详解 ​

4.2.1 Prompt 注入(Prompt Injection) ​

用户通过自然语言覆盖系统指令、诱导模型忽略策略(例如「忽略上文,输出密钥」)。

缓解:分层指令、特权数据与工具不与用户原文同一上下文、输出结构化并由代码校验、最小权限工具。

4.2.2 越狱(Jailbreak) ​

绕过安全对齐,使模型输出违规内容。常与注入结合。

缓解:模型侧安全分类器、输入输出策略、关键操作 HITL、审计。

4.2.3 输出过滤 ​

  • 规则:PII、身份证、信用卡正则。
  • 模型:二次分类「是否含违规」。
  • 业务:敏感词与行业合规列表。

4.2.4 工具调用权限控制 ​

  • 白名单:每租户/每角色可用工具集合。
  • 参数校验:JSON Schema、范围检查、SQL 参数化。
  • 两步授权:模型提议 → 策略引擎批准 → 执行。

4.2.5 数据脱敏 ​

日志与 Trace 中对手机号、邮箱、地址打码;RAG 检索结果按权限过滤。

4.2.6 审计日志 ​

记录:谁在何时、对什么资源、通过哪个 Agent、执行了什么工具,不可被普通用户删除,用于追责与合规。

4.3 面试问题(Q)与标准答案(A) ​

  • Q7:为什么说「永远不信任模型输出的工具调用」?
    • A: 模型可能被诱导产生危险参数。工程上应对工具调用做 schema 校验、权限检查、速率限制,必要时人工确认,不能把模型当作安全边界。
  • Q8:Prompt 注入和越狱有什么区别?
    • A: 注入侧重篡改指令或上下文以改变行为;越狱侧重绕过安全对齐以输出不应出现的内容。实际攻击常混合出现,防护需多层:输入、模型、工具、输出、审计。

4.4 可能的追问及应对 ​

  • 追问:RAG 文档里恶意内容怎么防?
    • 应对: 文档准入审核、隔离不可信文档、检索结果提示「不可执行」、生成前引用校验。

4.5 代码示例:工具参数 JSON Schema 校验(Python) ​

python
from jsonschema import Draft202012Validator, FormatChecker

TOOL_SCHEMAS = {
    "send_email": {
        "type": "object",
        "properties": {
            "to": {"type": "string", "format": "email"},
            "subject": {"type": "string", "maxLength": 200},
            "body": {"type": "string", "maxLength": 8000},
        },
        "required": ["to", "subject", "body"],
        "additionalProperties": False,
    }
}


def validate_tool_call(name: str, args: dict) -> None:
    schema = TOOL_SCHEMAS.get(name)
    if not schema:
        raise ValueError("unknown_tool")
    v = Draft202012Validator(schema, format_checker=FormatChecker())
    errors = sorted(v.iter_errors(args), key=lambda e: e.path)
    if errors:
        raise ValueError(errors[0].message)
from jsonschema import Draft202012Validator, FormatChecker

TOOL_SCHEMAS = {
    "send_email": {
        "type": "object",
        "properties": {
            "to": {"type": "string", "format": "email"},
            "subject": {"type": "string", "maxLength": 200},
            "body": {"type": "string", "maxLength": 8000},
        },
        "required": ["to", "subject", "body"],
        "additionalProperties": False,
    }
}


def validate_tool_call(name: str, args: dict) -> None:
    schema = TOOL_SCHEMAS.get(name)
    if not schema:
        raise ValueError("unknown_tool")
    v = Draft202012Validator(schema, format_checker=FormatChecker())
    errors = sorted(v.iter_errors(args), key=lambda e: e.path)
    if errors:
        raise ValueError(errors[0].message)

5. 部署与运维 ​

5.1 概念解释 ​

把 Agent 服务做成可扩容、可回滚、可观测的标准微服务或 Job。模型权重多在云端 API,自托管时需 GPU 与专用推理栈;应用层仍普遍容器化 + K8s。

5.2 原理详解 ​

5.2.1 Docker 容器化 ​

  • 镜像:应用代码 + 依赖,环境一致。
  • 环境变量:密钥、路由配置、功能开关。
  • 健康检查:/health 探活,避免流量打到未就绪 Pod。

5.2.2 Kubernetes 部署 ​

  • Deployment:副本数、滚动更新。
  • HPA:按 CPU/QPS 扩容。
  • Service/Ingress:对外暴露与 TLS。
  • ConfigMap/Secret:配置与密钥分离。

5.2.3 CI/CD 流水线 ​

  • CI:lint、单测、镜像构建、漏洞扫描。
  • CD:灰度发布、自动回滚(健康检查失败)。

5.2.4 蓝绿部署与金丝雀发布 ​

  • 蓝绿:两套环境切换,回滚快,资源占用高。
  • 金丝雀:小流量新版本,指标异常则回滚,风险更可控。

5.2.5 模型版本管理 ​

Prompt 版本、模型名版本、工具清单版本一并记录到 Trace 与配置。

不可变发布:v20260401 标签,避免「同名不同行为」。

5.2.6 A/B 测试 ​

对 Prompt、模型、工具策略分流用户,比较任务成功率、成本、延迟、满意度;需统计显著性,避免早停误判。

5.3 面试问题(Q)与标准答案(A) ​

  • Q9:金丝雀发布要关注哪些指标?
    • A: 错误率、P95 延迟、Token 成本、业务指标(任务完成率、用户投诉)。Agent 还应看工具失败率、重试率、熔断率。

5.4 可能的追问及应对 ​

  • 追问:Agent 长任务怎么做部署不中断?
    • 应对: 任务队列与 Worker、可恢复状态(checkpoint)、K8s 优雅停机(完成手头消息再退出)。

5.5 代码示例:极简 Dockerfile 与 Kubernetes Deployment 片段 ​

Dockerfile(应用镜像骨架):

dockerfile
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s CMD curl -fsS http://127.0.0.1:8000/health || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s CMD curl -fsS http://127.0.0.1:8000/health || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

deployment.yaml(单副本示意,生产请调 probes 与 resources):

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: agent-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: agent-api
  template:
    metadata:
      labels:
        app: agent-api
    spec:
      containers:
        - name: api
          image: your-registry/agent-api:v20260401
          ports:
            - containerPort: 8000
          envFrom:
            - secretRef:
                name: llm-keys
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 5
apiVersion: apps/v1
kind: Deployment
metadata:
  name: agent-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: agent-api
  template:
    metadata:
      labels:
        app: agent-api
    spec:
      containers:
        - name: api
          image: your-registry/agent-api:v20260401
          ports:
            - containerPort: 8000
          envFrom:
            - secretRef:
                name: llm-keys
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 5

6. 性能优化 ​

6.1 概念解释 ​

Agent 的瓶颈常在串行 LLM 调用、工具 RTT、过大上下文。优化方向:并行、流式、限流、缓存、连接复用。

6.2 原理详解 ​

6.2.1 异步处理 ​

使用 asyncio 或消息队列,I/O 等待不阻塞线程;多工具无依赖时可并行。

6.2.2 流式输出(Streaming) ​

边生成边返回 SSE,首字时间(TTFB)下降,用户体验好;注意中间状态与最终答案一致性展示。

6.2.3 并发控制 ​

用 Semaphore 限制同时 LLM 调用数,防止把自家或供应商打满触发 429。

6.2.4 连接池 ​

HTTP 客户端复用 keep-alive;数据库用连接池。减少握手开销。

6.2.5 缓存分层 ​

  • L1:进程内 LRU(极低延迟,单机)。
  • L2:Redis(多实例共享,需 TTL 与序列化)。
  • 读路径:L1 → L2 → 回源模型。

6.3 面试问题(Q)与标准答案(A) ​

  • Q10:Streaming 会影响计费或日志吗?
    • A: 计费仍以 Token 为准;日志需聚合完整响应再记一条,或记增量 chunk 并关联 trace_id。注意流式中途断开时的部分结果处理。

6.4 可能的追问及应对 ​

  • 追问:并行工具调用怎么保证顺序?
    • 应对: 若展示需要顺序,可按依赖图拓扑执行;无关则并行,合并结果时带 step_id。

6.5 代码示例:异步 + 信号量 + 简易双层缓存 ​

python
import asyncio
import time


class TwoLevelCache:
    def __init__(self):
        self.l1 = {}
        self.redis = None  # 接入 redis.asyncio

    async def get(self, key: str):
        if key in self.l1:
            return self.l1[key]
        # val = await redis.get(key)
        return None

    async def set(self, key: str, value: str, ttl: int = 300):
        self.l1[key] = value
        # await redis.setex(key, ttl, value)


async def limited_llm_call(sema: asyncio.Semaphore, fn, *args, **kwargs):
    async with sema:
        return await fn(*args, **kwargs)
import asyncio
import time


class TwoLevelCache:
    def __init__(self):
        self.l1 = {}
        self.redis = None  # 接入 redis.asyncio

    async def get(self, key: str):
        if key in self.l1:
            return self.l1[key]
        # val = await redis.get(key)
        return None

    async def set(self, key: str, value: str, ttl: int = 300):
        self.l1[key] = value
        # await redis.setex(key, ttl, value)


async def limited_llm_call(sema: asyncio.Semaphore, fn, *args, **kwargs):
    async with sema:
        return await fn(*args, **kwargs)

7. 评估与测试 ​

7.1 概念解释 ​

没有度量就没有优化。Agent 评估要覆盖:对不对(准确性)、稳不稳(鲁棒性)、快不快与贵不贵(效率),并结合线上真实分布持续回归。

7.2 原理详解 ​

7.2.1 Agent 评估维度 ​

维度含义示例指标
准确性任务是否完成、答案是否正确人工标注、LLM-as-judge(需谨慎)
鲁棒性噪声、对抗、边界输入成功率方差、最坏 case
效率步数、延迟、费用平均工具调用次数、Token

7.2.2 自动化测试框架 ​

  • 单元测试:工具参数解析、路由逻辑、熔断状态机。
  • 集成测试:Mock LLM 固定输出,验证编排是否符合预期。
  • 端到端:沙箱环境 + 小型真实调用(控制成本)。

7.2.3 基准测试(Benchmark) ​

固定数据集与评分器,对比不同 Prompt/模型/策略;注意数据泄漏(测试集进训练)与评分器偏差。

7.2.4 人工评估 vs 自动评估 ​

  • 人工:金标准,贵且慢。
  • 自动:快,可能偏袒某风格。

实践:自动跑大规模筛选,人工抽审争议样本。

7.2.5 A/B 测试与回归测试 ​

  • A/B:线上分流,看业务与成本。
  • 回归:新模型/Prompt 必须在评估集上不低于基线再发布。

7.3 面试问题(Q)与标准答案(A) ​

  • Q11:用 LLM 给 LLM 打分有什么坑?
    • A: 可能偏好冗长、格式讨好;与裁判模型强相关。应对:人机混合、多裁判投票、规则评分结合、对抗样本集。
  • Q12:如何设计 Agent 的回归测试集?
    • A: 覆盖主路径、典型失败、工具错误、长上下文、多语言;每条用例含输入、期望工具或期望答案要点、不可出现项;版本化并与 CI 集成。

7.4 可能的追问及应对 ​

  • 追问:线上指标与离线评估不一致?
    • 应对: 数据分布偏移、用户更「难」、工具线上权限不同;建立 slice 分析与线上采样标注。

7.5 代码示例:pytest 集成测试(Mock LLM) ​

python
import json

import pytest


class FakeLLM:
    def __init__(self, replies):
        self.replies = iter(replies)

    def chat(self, messages):
        return next(self.replies)


def run_agent_stub(user_goal: str, tools: dict, llm: FakeLLM) -> str:
    """最小编排:第一轮让模型返回 tool JSON,第二轮返回最终答案。"""
    first = llm.chat([])
    # 简化:假定 first 就是要调用的 JSON 字符串
    action = json.loads(first)
    obs = tools[action["tool"]](**action["args"])
    second = llm.chat([{"role": "user", "content": str(obs)}])
    return second


def test_agent_calls_tool_once():
    replies = [
        '{"tool":"search","args":{"q":"Python"}}',
        "最终答案基于工具结果。",
    ]
    calls = []

    def search(q: str):
        calls.append(q)
        return ["doc1"]

    tools = {"search": search}
    out = run_agent_stub("查 Python", tools, FakeLLM(replies))
    assert calls == ["Python"]
    assert "最终" in out
import json

import pytest


class FakeLLM:
    def __init__(self, replies):
        self.replies = iter(replies)

    def chat(self, messages):
        return next(self.replies)


def run_agent_stub(user_goal: str, tools: dict, llm: FakeLLM) -> str:
    """最小编排:第一轮让模型返回 tool JSON,第二轮返回最终答案。"""
    first = llm.chat([])
    # 简化:假定 first 就是要调用的 JSON 字符串
    action = json.loads(first)
    obs = tools[action["tool"]](**action["args"])
    second = llm.chat([{"role": "user", "content": str(obs)}])
    return second


def test_agent_calls_tool_once():
    replies = [
        '{"tool":"search","args":{"q":"Python"}}',
        "最终答案基于工具结果。",
    ]
    calls = []

    def search(q: str):
        calls.append(q)
        return ["doc1"]

    tools = {"search": search}
    out = run_agent_stub("查 Python", tools, FakeLLM(replies))
    assert calls == ["Python"]
    assert "最终" in out

8. 幻觉问题的工程解决方案 ​

8.1 概念解释 ​

幻觉:模型生成看似合理但事实错误的内容。Agent 场景下危害更大(错误决策、错误工具参数)。工程上通常事前—事中—事后三层治理。

8.2 原理详解 ​

8.2.1 事前预防 ​

  • RAG:用检索到的可靠片段 grounding。
  • 结构化 Prompt:要求「仅依据引用回答」「无依据则说不知道」。
  • 知识边界:限定领域与数据源。

8.2.2 事中控制 ​

  • 置信度过滤:对分类/抽取任务输出概率或自评(注意校准)。
  • 工具验证:计算、查询、代码执行以外部世界为准。

8.2.3 事后校验 ​

  • 事实核查:检索交叉验证、与数据库比对。
  • 引用标注:答案逐句带来源编号,便于人工审核。

8.3 面试问题(Q)与标准答案(A) ​

  • Q13:只靠「请诚实回答」能否解决幻觉?
    • A: 不能作为唯一手段。需 RAG/工具/校验与系统提示结合,并对关键场景拒答或降级。
  • Q14:RAG 一定能降幻觉吗?
    • A: 不一定。若检索质量差、重排序失败、模型忽略上下文,仍可能错答。需检索评估、重排序、引用约束、拒答策略。

8.4 可能的追问及应对 ​

  • 追问:工具结果与检索矛盾听谁的?
    • 应对: 定义优先级(权威数据库 > 实时工具 > 检索片段);矛盾时输出不确定并建议人工。

8.5 代码示例:带引用约束的 Prompt 片段 ​

text
你是企业内部助手。仅允许使用「上下文引用」中的事实回答问题。

规则:
1. 每一句事实陈述末尾标注引用编号,如 [1][2]。
2. 若上下文不足以回答,输出:「根据已有资料无法确定」,并列出需要补充的信息。
3. 禁止编造未出现在上下文中的数字、日期、人名。

上下文引用:
[1] {{snippet_1}}
[2] {{snippet_2}}

用户问题:{{user_query}}
你是企业内部助手。仅允许使用「上下文引用」中的事实回答问题。

规则:
1. 每一句事实陈述末尾标注引用编号,如 [1][2]。
2. 若上下文不足以回答,输出:「根据已有资料无法确定」,并列出需要补充的信息。
3. 禁止编造未出现在上下文中的数字、日期、人名。

上下文引用:
[1] {{snippet_1}}
[2] {{snippet_2}}

用户问题:{{user_query}}

9. 综合面试题库(15+ 题) ​

下列题目覆盖本篇各模块,答案要点可直接用于面试口述。

  • Q15:你会如何设计一个多模型网关的架构?
    • A: 统一 Adapter 抽象;配置中心维护模型能力与配额;入口做鉴权与租户路由;核心做优先级调度 + 熔断 + 重试退避 + 降级链;全链路 Trace 与 usage 计费;密钥按租户与环境隔离。
  • Q16:指数退避为什么要加 jitter?
    • A: 避免大量客户端在同一时刻齐刷刷重试,造成重试风暴;抖动把时间错开,减轻服务端同步压力。
  • Q17:语义缓存如何保证安全?
    • A: 分租户隔离、键含模型与 Prompt 版本、相似度阈值保守、敏感任务禁用或二次确认、TTL 与主动失效。
  • Q18:OpenTelemetry 在 Agent 里一般打哪些 Span?
    • A: 顶层请求 Span;子 Span 包括每次 LLM、检索、工具、重试、降级;记录属性如 model、token、tool.name、error.type。
  • Q19:工具调用的「两步授权」怎么做?
    • A: 模型仅生成「意图与参数」→ 策略服务校验角色、资源 ID、速率 → 执行器真正调用;拒绝时把原因反馈给模型或用户。
  • Q20:蓝绿与金丝雀如何取舍?
    • A: 蓝绿适合二进制切换、快速回滚、可接受双倍资源;金丝雀适合渐进验证、对错误更敏感的生产流量,资源占用更平滑。
  • Q21:K8s 部署 Agent 服务时 HPA 可以按什么指标扩?
    • A: CPU/内存、自定义指标如请求队列长度、P95 延迟、429 比例(需 Prometheus Adapter);注意冷启动与 LLM 长尾延迟。
  • Q22:异步一定能提高 Agent 吞吐吗?
    • A: 对 I/O 密集(HTTP、DB)通常能;若受 GPU 或单线程推理限制,需配合批处理、多副本、队列;还要防止无界并发压垮下游。
  • Q23:如何监控一次 Agent 任务的「真实成本」?
    • A: 汇总每步 prompt+completion tokens × 单价;加上检索与向量库费用;分摊基础设施;按租户与功能维度出报表与预算告警。
  • Q24:评估集泄露怎么防?
    • A: 严格版本管理;开发与训练数据隔离;禁止把测试集写进 Prompt 示例;定期刷新测试用例。
  • Q25:线上发现幻觉率升高,你如何排查?
    • A: 先看是否模型/Prompt/RAG 索引近期变更;抽样 Trace 看检索命中与引用;检查工具失败降级是否变多;对比离线评估集与线上 slice。
  • Q26:小型团队没有 LangSmith,最小可观测方案是什么?
    • A: 结构化日志 + trace_id + 每次 LLM/工具的耗时与 token;Sentry 捕获异常;用 OpenTelemetry 导出到 Jaeger 或云厂商 APM;评估用 CSV 用例 + CI 脚本。
  • Q27:如何做「关键路径」与「非关键路径」分级?
    • A: 关键路径(支付、删数据)强模型 + HITL + 审计;非关键(草稿、摘要)小模型 + 宽松超时 + 积极缓存。
  • Q28:并发控制 Semaphore 设多大?
    • A: 结合供应商 RPM/TPM、本机 CPU、下游工具容量;压测得到饱和点,略低于饱和并留余量;按租户分桶避免噪声邻居。
  • Q29:为什么 Agent 更需要「版本化」的 Prompt 与模型?
    • A: 行为随 Prompt/模型悄悄变化会导致线上回归难定位;版本化可与 Trace、评估集、回滚策略一一对应。

本篇追问应对(通用话术) ​

  • 追问:「你们规模小也要上这么多吗?」
    • 应对: 按阶段取舍:先有日志与 trace_id,再有熔断与预算,再上完整评估平台;原则是越早埋点成本越低。
  • 追问:「这些会不会拖慢响应?」
    • 应对: 日志异步、Trace 采样、热路径仅记必要字段;观测开销应可配置、可降级。

小结检查清单(面试前自测) ​

  • [ ] 能画出:路由 → 熔断 → 重试 → 降级 → 计费 的闭环
  • [ ] 能解释 Closed/Open/Half-Open 与探测流量
  • [ ] 能说出 Token 估算与账单对账的差异
  • [ ] 能列举 Trace 上应挂的 Span 类型
  • [ ] 能描述工具调用的权限与校验分层
  • [ ] 能说清金丝雀与蓝绿的适用场景
  • [ ] 能说明 L1/L2 缓存与语义缓存风险
  • [ ] 能口述幻觉治理的事前—事中—事后

祝面试顺利。