如果你把 AI Agent 理解为“LLM + 一堆工具”,你会在两个地方翻车:
- 系统不可控:它会跑偏、会胡说、会乱调用工具。
- 系统不可靠:外部 API 超时、限流、错误返回时,你的 Agent 会直接崩溃或给出更糟的结果。
面试官考 Agent,核心就是在考:你是否理解“把不确定模型变成可控系统”的方法。
这篇文章不讨论“某个框架怎么用”,而是用工程语言把 Agent 的三大模块讲透:
- 记忆:你到底把什么信息交给模型?怎么压缩?怎么检索?怎么隔离?
- 规划:你怎么把大任务拆成可执行的步骤?怎么检测跑偏?怎么纠错?
- 工具调度:你怎么在真实世界的不稳定条件下执行?怎么保证幂等?怎么做并发控制?
读完你至少应该能做到:
- 在白板上画出一个 Agent 的执行环(loop)和状态机
- 解释每个环节的失败模式,以及“工程化兜底”
- 设计一个最小 Demo,并写出一组可验证指标
一、先建立共识:Agent 的最小架构是什么?
你可以把 Agent 的执行看成一个循环(loop):
- 感知(Perception):读取用户输入 + 当前状态 + 相关记忆
- 决策(Decision):生成计划/下一步行动(plan/action)
- 执行(Action):调用工具或输出文本,并写入状态/记忆
最常见的“失控”发生在:决策与执行之间缺少约束,或状态与记忆不可验证。
为了更工程化地讨论,我们把一次执行称为一次 run:
run(user_input) => {
context = build_context(user_input, session_state, memory)
plan_or_action = decide(context)
result = act(plan_or_action)
validated = validate(result)
record(trace, metrics)
return output
}
关键点:每一步都应该产生可记录、可回放、可比较的证据,否则你无法迭代。
二、记忆机制:不是“存聊天记录”,而是“管理状态与证据”
2.1 你会遇到的三个硬约束
- 上下文窗口有限:你不可能把全部历史、全部文档都塞进去。
- 信息密度不均:一段对话里 90% 是噪声,10% 是约束条件。
- 多用户隔离:任何串话都是灾难级 bug。
所以“记忆”不是一个组件名,而是一组策略。
2.2 记忆的三层分工(面试必考)
建议用“三层记忆”去答题:
| 层级 | 你存什么 | 解决什么问题 | 常见坑 |
|---|---|---|---|
| 短期上下文(Short-term) | 最近 N 轮原文 | 保持语境 | 太长导致成本高/窗口爆炸 |
| 工作记忆(Working memory) | 摘要 + 结构化状态(目标/约束/已完成步骤) | 控制任务执行 | 摘要丢关键约束,导致跑偏 |
| 外挂记忆(External) | RAG 证据片段、业务数据、工具结果 | 补事实,降幻觉 | 检索噪声、引用不透明 |
面试官最爱追问的是:为什么需要“结构化状态”?
答案是:纯自然语言摘要无法当作“系统状态”,它不可校验、不可 diff、不可约束。
一个典型的结构化状态示例:
{
"goal": "生成出货单 PDF",
"constraints": {
"language": "zh-CN",
"currency": "USD",
"deadline": "2026-03-02"
},
"progress": [
{"step": "收集订单信息", "done": true},
{"step": "校验地址", "done": false}
],
"permissions": ["read:orders", "write:pdf"],
"last_tool_errors": []
}
结构化状态的价值:
- 可校验:缺字段就补,字段不合法就拒绝。
- 可约束:下一步计划必须引用这些约束。
- 可回放:你能解释“为什么走到这一步”。
2.3 窗口管理:摘要不是答案,摘要需要测试
很多系统的“记忆失败”不是因为没做摘要,而是因为摘要不可靠。
建议你在工程里把摘要当成一个可测试模块:
- 输入:完整历史
- 输出:摘要 + 结构化状态
- 测试:摘要必须保留的关键字段(目标、约束、已完成步骤、风险)
你可以为摘要定义一个“必须保留项”检查:
function assertSummary(summary: { goal?: string; constraints?: Record<string, unknown> }) {
if (!summary.goal) throw new Error('summary.missing_goal')
if (!summary.constraints) throw new Error('summary.missing_constraints')
}
这类“显得很工程”的做法,在面试里非常加分:你把 LLM 当成不稳定依赖,而不是神。
延伸阅读:
三、任务规划:Agent 不是“想一想”,而是“可执行计划 + 校验 + 纠错”
3.1 为什么规划是 Agent 的分水岭
没有规划的 Agent 很像“高级聊天机器人”:它能回答,但难以完成复杂任务。
有规划的 Agent 才像“系统”:它能把目标拆成步骤,并在失败时回退。
规划的工程化要求是:
- 计划必须结构化(能被程序解析)
- 每一步有成功条件(否则永远不知道是否完成)
- 允许纠错与重规划(任务拆错是常态)
3.2 最小可用的计划结构
下面的结构足够覆盖大部分面试题:
{
"steps": [
{
"id": "s1",
"tool": "searchDocs",
"input": {"query": "shipping address validation rules"},
"success": "找到规则并返回引用片段"
},
{
"id": "s2",
"tool": "validateAddress",
"input": {"orderId": "..."},
"success": "返回 status=valid 或 errors 列表"
}
],
"stop": {"maxSteps": 6, "deadlineMs": 45000}
}
你要强调:计划不是写给人看的,是写给系统执行的。
3.3 “拆错了怎么办”:规划的纠错闭环
这是面试官最常用来区分层次的问题。
一个成熟的答法是:把纠错拆成 3 个阶段。
- 检测:怎么知道跑偏?
- 输出不满足 schema/业务规则
- 工具返回的证据不足
- 计划步骤数异常增长(无限循环征兆)
- 定位:错在哪?
- 是目标理解错?(意图识别)
- 是证据不足?(检索/数据缺失)
- 是工具失败?(参数/权限/超时)
- 修复:如何回到可控状态?
- 重新构建上下文(补证据/补约束)
- 重规划(plan repair)
- 降级(解释性答复 + 让用户补信息)
工程上可以非常朴素:
- 最多重规划 1-2 次,超出就停止并请求人工/用户输入。
- 每次重规划必须写明原因(记录到 trace),否则无法复盘。
四、工具调度:Function Calling 的本质是“受控执行”
4.1 工具调度为什么比 prompt 更重要
在真实系统里,最糟糕的失败不是“模型答错”,而是:
- 模型调用了不该调用的工具(越权)
- 工具被重复调用造成副作用(重复下单、重复写库)
- 工具失败导致系统崩溃,用户无反馈
所以面试官会盯着问:你如何保证工具调用可控?
4.2 四件工程必需品:超时、重试、幂等、并发控制
1) 超时(Timeout)
- 每个 tool 调用都要有单独 timeout
- 整个 run 也要有 deadline(防止无限耗时)
2) 重试(Retry)
重试不是“失败就再来一次”。至少要做到:
- 只对可重试错误重试(网络抖动、429、临时 5xx)
- 指数退避(避免雪崩)
- 上限(比如最多 2 次)
3) 幂等(Idempotency)
凡是有副作用的工具(写库、下单、发邮件),必须有幂等键:
const idempotencyKey = `${runId}:${stepId}`
await tools.createInvoice({ orderId }, { idempotencyKey })
4) 并发控制(Concurrency)
- 同一用户同一资源,避免并发写冲突
- 全局并发上限,避免打爆下游
- 按租户/用户做隔离
4.3 工具参数错误:把“修复”做成回路
你会遇到这种情况:模型生成了无效参数。
成熟做法不是“失败就算了”,而是把错误回传给模型,让它修复:
- schema 校验失败 → 生成可读错误(缺字段/类型错/越界)
- 把错误作为约束重新喂给模型 → 让它产生新参数
- 限制重试次数,防止死循环
这背后的关键思想:不要让模型直接接触世界,让系统作为“执行防火墙”。
延伸阅读:
五、一个可落地的最小 Demo 路径(面试官最爱看这个)
你不需要写一个“万能 Agent”,只需要一个能证明你理解三大模块的 Demo。
5.1 Demo 目标
做一个“文档 + 工具”的任务型 Agent,例如:
- 用户输入:帮我根据订单号生成出货单
- Agent:检索规则(RAG)→ 调用工具查询订单 → 校验地址 → 生成 PDF
5.2 最小模块划分
MemoryStore:存结构化状态 + 摘要Retriever:RAG 检索证据Planner:生成结构化计划Executor:执行步骤(工具调度)Validator:schema/业务规则校验RunLogger:trace + metrics
5.3 伪代码骨架(面试可直接画)
interface ToolCall {
tool: string
input: Record<string, unknown>
}
interface PlanStep {
id: string
call: ToolCall
success: string
}
interface Plan {
steps: PlanStep[]
stop: { maxSteps: number; deadlineMs: number }
}
async function runAgent(userInput: string, sessionId: string) {
const runId = crypto.randomUUID()
const startedAt = Date.now()
const state = await memory.load(sessionId)
const evidence = await retriever.search(userInput)
const context = buildContext({ userInput, state, evidence })
const plan: Plan = await planner.makePlan(context)
validatePlan(plan)
for (const step of plan.steps.slice(0, plan.stop.maxSteps)) {
if (Date.now() - startedAt > plan.stop.deadlineMs) throw new Error('run.deadline_exceeded')
const result = await executor.execute(step, { runId })
const ok = await validator.check(step, result)
logger.recordStep({ runId, step, result, ok })
if (!ok) {
// 纠错:让 planner 解释失败原因并重规划(上限 1-2 次)
return await recoverOrDegrade({ runId, sessionId, userInput, state, evidence, step, result })
}
}
return logger.finalize(runId)
}
你不需要把每个模块都写到生产级,但你要能解释:
- state 是什么,怎么校验
- evidence 怎么来,如何引用
- plan 为什么要结构化
- executor 如何处理 timeout/retry/幂等
- validator 如何定义成功条件
六、评估与指标:让“变好”有证据
6.1 最小离线评估(你可以 1 天做出来)
准备一个 eval_cases.json:
- 10 条简单任务(无工具、无检索)
- 20 条工具任务(带失败注入)
- 20 条检索任务(故意混入噪声文档)
每次改 prompt / 改检索 / 改规划后,跑一遍回放,比较:
- 完成率
- 平均步数
- 工具失败导致的终止比例
- 引用证据比例
6.2 建议你在面试里能报出这 6 个数字
success_rate:任务完成率p95_latency_ms:P95 延迟avg_llm_calls:平均模型调用次数tool_timeout_rate:工具超时比例citation_rate:引用证据比例cost_per_run:每次 run 的成本估算
没有真实用户也没关系,有 eval 集就能自证。
七、你现在该怎么准备面试?(最短路径)
如果你时间有限,建议按这个顺序补:
- 先把“结构化输出 + schema 校验”练到顺手
- 再把“RAG + 引用证据 + 拒答”做成硬约束
- 最后把“工具调度的可靠性”补齐(timeout/retry/幂等/并发)
当你能把这三件事讲清楚,你就已经超过大量“只会搭积木”的候选人。
延伸阅读:


