第 1 章面试 · 01 LLM API 与模型调用基础
目标岗位:Agent Harness 研发/工程方向(参考
~/agent/jb/jd.md) 用法:先自己口头答一遍,再对照"参考回答";重点看"考察点"和"坑"。 原则:所有回答结论先行 → 项目证据 → 原理 → 边界与改进,不要背概念。
0. 面试官视角:这类题到底在考什么
同样的知识点,面试官会按三层递进考察:
- 概念层:懂不懂(Chat Completions 结构、token 是什么);
- 实践层:做没做过(你项目里怎么实现的,代码在哪);
- 判断层:为什么这么设计、哪里做得不好、怎么改(这是拉开差距的地方)。
回答公式:一句话结论 → 你项目里的实现/例子 → 背后的原理 → 边界与可改进点。 最后一步尤其重要——主动说"这里我还没做/做得不好,我的方案是 X",比被追问出来 强得多。
1. 按讲义章节的题目与参考回答
1.1 Chat Completions / Responses API
Q1:你们网关为什么对外暴露 chat.completions,而不是直接调各家 SDK?
参考回答:因为 chat.completions 是事实标准,DeepSeek、DashScope、Moonshot、 本地 Ollama 都提供兼容端点。我在 provider 层定义了统一接口
ChatProvider, 各家用 adapter 做格式转换(openai_compatible.py/anthropic.py/gemini.py),上层代码只认一个接口。收益是:换模型不改业务代码、 可以跨厂商回退、用量和错误统一统计。本质是把"协议差异"收敛到 adapter 层。
考察点:是否理解"统一抽象"的动机(协议差异、切换成本、回退、可观测)。
Q2:Chat Completions 和 Responses API 有什么区别?你们为什么没做 Responses?
参考回答:两者能力重叠,结构不同——Responses 用顶层
instructions+input,输出在output[].content[].text,参数叫max_output_tokens, 用量叫input_tokens/output_tokens。我没做 Responses 有两个原因: 一是我们主要接 DashScope/DeepSeek 等 OpenAI 兼容端点,它们目前以 chat.completions 为主;二是时间优先级,先把 chat.completions 链路做扎实。 如果要做,我会先做网关层 shim:/v1/responses接收 Responses 格式, 内部转成 chat.completions 走现有 provider,这样 OpenAI 新 SDK 也能直连。
加分点:能说出字段级映射(instructions↔system、max_output_tokens↔max_tokens、 input_tokens↔prompt_tokens),说明真的对比过协议。 坑:不要吹"我们支持 Responses"——项目里没有,面试官会追问细节。
Q3:Responses API 有什么好处?OpenAI 为什么推新接口?
参考回答:一句话——Chat Completions 是"无状态单轮请求",Responses 是 为 Agent 场景把状态、工具和内置能力做成服务端一等公民。我理解的好处有三: 一是多轮有状态,传
previous_response_id就能续上下文(配store: true), 不用每次重发整个 messages,省 token 也省客户端状态管理;二是输出统一成output[]items(message / function_call / reasoning / web_search_call), 工具调用的编排比散落在choices[0].message.tool_calls里清晰;三是 built-in tools(web_search、code_interpreter、file_search)直接当参数传, 不用自己实现。OpenAI 推它的真实动机是产品演进:Assistants API 已弃用并 将在 2026-08 移除,能力并入 Responses,新特性(reasoning、memory、实时) 优先放这里。对我们的启示:协议选型要跟着生态走——DeepSeek/DashScope 目前 还是 chat.completions,所以网关以它为准;将来用 shim 兼容两种协议即可。
加分点:能说出 previous_response_id、built-in tools、Assistants 弃用合并, 说明理解的是"产品演进",不是"换个字段名"。 坑:别说"Responses 更快更便宜"这种没依据的话——有状态续接省的是重发 历史输入的 token 和客户端复杂度,不是推理本身变便宜。
Q4:多轮对话为什么必须把 assistant 的历史回复一起传回去?
参考回答:LLM 是无状态函数,每轮都靠 messages 里的完整上下文重建对话。 更隐蔽的一点:有些模型(比如 DeepSeek 思考型、Qwen 的 reasoning)要求 历史里的 reasoning 内容也要回传,否则会报 400"必须回传 reasoning_content"。 我调研时在 DeerFlow、Hermes 的代码里都看到过这个坑,说明这是真实工程问题 而不是文档里才有的。
考察点:是否理解"无状态 + 上下文重建";能否举出 reasoning 回传这种真实坑。
Q5:finish_reason 有哪些?你们网关怎么处理的?
参考回答:常见有
stop(正常结束)、length(被 max_tokens 截断)、content_filter(被安全策略过滤)、tool_calls(请求调用工具)。 我们的网关目前组装响应时固定返回stop(openai_format.py),上游的length还没透传——这是一个已知的改进点,我的方案是把上游 finish_reason 透传到网关响应,让调用方能区分"正常结束"和"被截断"。
坑:如果只说"stop 是正常结束"就停,属于概念层回答;主动说出自己项目 “没透传、怎么补"才到判断层。
1.2 模型参数
Q6:temperature 和 top_p 有什么区别?为什么建议只改一个?
参考回答:temperature 调整整个概率分布的"软硬程度”(低→更取最高概率词), top_p 是核采样——只从累计概率达到 p 的最可能词里选。两者都在控制随机性, 同时调会互相干扰,所以 OpenAI 建议二选一。工程上:结构化输出/查词用低 temperature 保证稳定,创意任务用高 temperature。
Q7:max_tokens 设置太小会发生什么?怎么处理?
参考回答:回答被截断,上游返回
finish_reason: length。处理三步: 加预算、压缩输入(摘要/分块)、或换 context window 更大的模型。 我们网关也暴露了 max_tokens,页面/讲义里就有max_tokens=10复现截断的例子。
Q8:你们为什么没暴露 top_p?如果让你加,怎么加?
参考回答:当前只透传了 temperature 和 max_tokens,top_p 属于已知扩展项。 加的话:
ChatRequest加top_p字段 → provider 的 complete/stream 加参数 → OpenAI 兼容协议直接透传;Anthropic/Gemini 原生不支持或语义不同,需要单独映射 或文档说明。工作量主要在接口透传和各家映射,而不是概念本身。
考察点:对"接口演进"有方案,而不是只会用现成参数。
1.3 Token 与 Context Window
Q9:一个 token 大概多少字符?中文呢?怎么算成本?
参考回答:英文约 4 字符 ≈ 1 token,中文约 1 字 ≈ 1~2 token,标点空格也占。 计费按 prompt + completion 分开算,输出通常比输入贵。精确计数用 tiktoken 这类分词器(首次用需下载编码表),我在讲义里给了估算函数。成本侧,我们的
usage.py有按模型名前缀匹配的价格表,每次调用记录 token 并估算成本—— 强调一下:那是估算,正式计费要以厂商账单为准。
Q10:context window 超了怎么办?
参考回答:三个手段——分块(长文按段落切,每次只喂相关块)、摘要(旧对话压成 summary)、缓存(固定 system prompt 命中 prompt cache 降本)。我们 Phase 0 的文章分块就是这个方向,还没实现;代码里
fits_context()可以预检预算。 还可以做"窗口预检":models.json 里给模型加 context_window 字段,请求前先 估算 token,超了直接提示而不是等上游报错。
加分点:主动提出"窗口预检"这类把概念工程化的想法。
Q11:你们怎么统计用量和成本?
参考回答:三层——provider 每次调用记录
last_usage(各家原始形状归一成 prompt/completion/total);网关把每次请求写成 UsageRecord(含延迟、成本、 错误);/v1/usage/summary和/v1/usage/recent提供聚合与明细,可落 JSONL。这是我面试里能直接演示的部分:页面上发一条消息,用量面板就有记录。
1.4 Structured Output
Q12:让模型返回 JSON 有哪几种方法?可靠性怎么排?
参考回答:三种,由弱到强——prompt 约定(“只输出 JSON”,可能被加注释/文字)、 JSON Schema / 原生 response_format(模型侧约束)、Pydantic 校验兜底(拿到结果 后再解析校验)。工程上我会"原生约束 + 校验兜底"组合:原生 response_format 保证 大概率合法,Pydantic 保证一定可解析,解析失败再重试或报错。
Q13:你们三家厂商分别怎么实现结构化输出的?
参考回答:OpenAI 兼容直接透传
response_format;Gemini 映射成generationConfig.responseMimeType: application/json+responseSchema; Anthropic 没有 response_format,我用隐藏工具强制:定义structured_output工具 +tool_choice锁定,再从tool_use.input提取 JSON。测试在test_structured_output.py,三家都有用例。一个已知边界:Anthropic 的 流式 + 结构化输出我暂时不支持(会直接报错提示用非流式),后续可以补input_json_delta的解析。
考察点:三家协议的差异和各自的实现路径;主动暴露 Anthropic 流式的边界。
Q14:模型返回的 JSON 解析失败怎么办?
参考回答:先分类——是模型输出了非法 JSON(少括号、加了说明文字),还是 结构对但字段缺失/类型错。前者可以用修复工具(如 json_repair,我在调研 nanobot 时看到它这么处理)或重新生成;后者用 Pydantic 校验兜底。我们的 provider 层对"响应缺少 choices/message/content"也会抛 ProviderError, 不会把脏数据传上去。
1.5 错误处理、重试与限流
Q15:你们错误分类怎么设计的?为什么鉴权错误不重试?
参考回答:
classify_provider_error把错误归为稳定类别——鉴权、限流、额度、 超时、网络、服务器、无效请求、内容过滤。关键判断:可重试/可回退的错误才重试 (网络/超时/限流/额度/5xx),鉴权和内容过滤重试没用,直接抛给上层,避免浪费 配额和时间。这在 fallback 里也复用:主模型 401 不会去试 fallback。
Q16:重试策略具体是什么?
参考回答:对 429 和 5xx 做指数退避(0.5s → 1s → 2s…),次数由
ANNA_MAX_RETRIES配置。我自己实现的_request_json里就是这个逻辑; 讲义里还演示了等价的call_with_retry,方便讲原理。补充一点:流式场景重试 要小心,已经吐出去的内容不能重复,我们 fallback 也遵循"已流式输出就不回退"。
Q17:熔断器为什么需要?怎么实现的?
参考回答:重试只解决"偶发抖动",解决不了"端点已经坏了"。所以主 provider 连续失败 3 次就熔断(
FallbackProvider),冷却 60 秒内直接走 fallback, 冷却结束半开一次探测,成功就复位。这是借鉴 nanobot 的设计,参数化 (threshold/cooldown)方便调整。
Q18:限流怎么做的?超限返回什么?
参考回答:按客户端 key 的令牌桶(
server/rate_limit.py),ANNA_RATE_LIMIT_RPM配置每分钟额度,超限返回 429 +Retry-After。演示时可以打开限流开关连打几条 看 429。如果上游也限流,那是另一层——上游 429 走重试和模型切换。
1.6 多模型调用
Q19:你们的模型路由有几种方式?
参考回答:三种——
auto(按 models.json 清单选最近到期候选)、厂商:模型显式路由(dashscope:qwen3.7-max)、纯模型名按关键词自动识别 (qwen*→dashscope、claude*→anthropic、llama*→ollama)。路由结果交给get_provider_chain,还能叠加跨厂商回退。讲义里router.resolve有可直接 演示的断言例子。
Q20:为什么免费模型按到期时间排序?清单怎么更新?
参考回答:免费测试模型有额度期限,按到期升序排,先把快过期的额度用完, 失效自动切下一个。清单是
models.json,三要素(模型/额度/到期),平台刷新后 直接改文件,运行期 mtime 检测自动热更新,不用重启。这借鉴了 pi 项目的 models.json 热更新思路。
Q21:新增一个厂商要改多少代码?
参考回答:协议兼容的厂商(比如新接一个 OpenAI 兼容服务)只需在
specs.py加一行 ProviderSpec(默认端点、key 环境变量、模型关键词); 协议不同的才需要新写 adapter。因为我把厂商差异做成了"声明式数据 + 少量钩子", 而不是散落的 if 分支。
Q22:本地模型怎么接的?
参考回答:Ollama/vLLM/LM Studio 都有 OpenAI 兼容端点,所以复用同一个 OpenAI 兼容 adapter,只是 base_url 指向 localhost、不需要 key (
OllamaProvider就是这么实现的)。list_models()还能探测本地已部署的模型。 如果遇到本地服务特有行为(比如 vLLM 的 reasoning 字段要保留回传), 就在 adapter 里打补丁——DeerFlow 的 vLLM provider 就是这么干的。
2. 通用回答技巧(怎么答能拿 offer)
2.1 用 PREP 结构
- Point:一句话结论(“协议不同,所以用 adapter 收敛”);
- Reason:原理(为什么这么设计);
- Example:你项目里的实现/测试/演示(这是别人没有的);
- Point:回到结论 + 边界与改进。
2.2 准备"弹药库"(每个都能现场演示/指路)
- 网关 + 可视化测试台(首页四个面板:对话/模型/模板/用量);
- 流式 vs 非流式对比演示(首字节耗时、分片表);
- 结构化输出三家实现 + 测试文件;
- 熔断/回退/限流都是可配置、有测试的;
models.json热更新 +list_models.py。
2.3 诚实边界清单(主动说,别等被问)
- Responses API 没做(有 shim 方案);
- top_p 未暴露(有透传方案);
- finish_reason 网关固定 stop(有透传方案);
- Anthropic 流式 + 结构化输出不支持(有 input_json_delta 方案);
- 分块/文章导入、prompt cache 属 Phase 0/后续;
- 用量成本是估算,以厂商账单为准;
- 模板和用量目前是内存态(用量可落 JSONL)。
面试官不扣"没做",扣"没做却吹做了"和"没做也不说下一步"。
2.4 反问环节(展示工程品味)
- “贵司 Harness 的模型路由和成本治理是怎么做的?”
- “线上模型调用有统一的错误分类/回退策略吗?怎么保证可观测?”
- “你们对 Responses API 和 function calling 的取舍是?”
3. 一分钟项目介绍(开场自述模板)
“我最近在做一个叫 Anna 的项目,目标是用 Agent 工程的方式搭一个 AI 英语阅读 伴侣,同时把它当训练场对齐 Agent Harness 岗位的能力要求。第一阶段的成果是一个 LLM 统一模型调用服务:OpenAI 兼容网关,下面接 DashScope、DeepSeek、Anthropic、 Gemini 和本地 Ollama,统一接口 + adapter + 注册表 + 声明式厂商元数据; 支持流式、结构化输出、模型路由、跨厂商回退熔断、用量成本统计。设计上参考了 nanobot、opencode、pi 这些开源实现,每个模块都有测试。我最看重的是把概念做成 可演示、可量化的东西——比如免费模型按到期时间自动轮换、失败自动切换模型, 都是配置驱动、有测试覆盖的。”