目标岗位:Agent Harness 研发/工程方向(参考 ~/agent/jb/jd.md) 用法:先自己口头答一遍,再对照"参考回答";重点看"考察点"和"坑"。 原则:所有回答结论先行 → 项目证据 → 原理 → 边界与改进,不要背概念。


0. 面试官视角:这类题到底在考什么

同样的知识点,面试官会按三层递进考察:

  1. 概念层:懂不懂(Chat Completions 结构、token 是什么);
  2. 实践层:做没做过(你项目里怎么实现的,代码在哪);
  3. 判断层:为什么这么设计、哪里做得不好、怎么改(这是拉开差距的地方)。

回答公式:一句话结论 → 你项目里的实现/例子 → 背后的原理 → 边界与可改进点。 最后一步尤其重要——主动说"这里我还没做/做得不好,我的方案是 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(请求调用工具)。 我们的网关目前组装响应时固定返回 stopopenai_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 属于已知扩展项。 加的话:ChatRequesttop_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 这些开源实现,每个模块都有测试。我最看重的是把概念做成 可演示、可量化的东西——比如免费模型按到期时间自动轮换、失败自动切换模型, 都是配置驱动、有测试覆盖的。”