Harness 项目讲解
Agent / Harness 面试准备(重中之重)
本篇保留项目口述与历史复盘原稿。当前实现与计划边界以 Agent 分级题库 和 交接说明 为准。
用法:第 1 节背熟(自我介绍里的项目版本);第 2、3 节是「演进史」,面试官问「你怎么理解 Agent」时从这里讲; 第 4、5 节是项目深挖,每个点都按「遇到什么问题 → 怎么改 → 取舍 → 数据」讲;第 6 节是高频题速答;第 7 节是踩坑故事。
所有数字都来自 xknow-ai 的代码注释和设计文档,面试时可以放心说;被追问细节时能落到具体机制上。
1. 项目口述稿
30 秒版
我现在在做一个企业 AI 工作台,后端是我主力写的。核心是一个自研的 Agent 运行时:一开始我们用 LangChain 和 Agno 编排固定流程,后来模型能力上来了,流程反而成了限制,我就参考 Claude Code 的思路改成了单循环 —— 模型决定调什么工具,运行时负责执行、回填、管上下文。之后围绕这个循环补了上下文压缩、prompt 缓存、子 agent、Skill、MCP、沙箱这些东西,还有一层统一的模型接入,管限流、计费和兜底。
2 分钟版(按这个顺序讲,每段一两句)
- 产品:中小企业上传文档、接入数据库和飞书企微,对话里让 Agent 查资料、查数、写文档、出看板、跑定时任务。
- 为什么自研:框架的固定工作流(先检索、再判断、再生成)在模型变强后成了天花板:用户的问题千奇百怪,流程覆盖不全;而且框架抽象太厚,出问题要钻到框架源码里查。function calling 已经足够稳定,一个 while 循环就是 Agent 的本体,剩下的价值全在「循环外面那圈东西」—— 这就是 harness。
- harness 里我做了什么:工具注册与按需加载、上下文压缩、prompt 缓存、并行工具、打转守卫、子 agent、Skill、MCP、沙箱、流式与断线重放、取消 / 插话 / 停止。
- 模型层:多供应商限流、按输出计量、兜底链、熔断;后来去掉 litellm 只用官方 SDK。
- 一个有代表性的问题:压缩阈值算错导致缓存命中率从 90% 掉到 35%(或者讲打转守卫那次 2780 万 token),说明我是看线上数据改设计的。
2. Agent 演进史(行业视角)
面试官问「你怎么理解 Agent / harness」时,用这条时间线讲,最后落到「所以我们的项目也走了一遍同样的路」。日期记到年月即可,个别说「大约」。
| 阶段 | 时间 | 代表 | 核心机制 | 解决了什么 | 留下的问题 |
|---|---|---|---|---|---|
| 0. 只有补全 | 2020–2022 | GPT-3 | 文本续写 + few-shot | 能生成文本 | 不能行动,没法接外部系统 |
| 1. 提示词里的 Agent | 2022 | CoT(2022.01)、ReAct(2022.10)、早期 LangChain | 让模型按 Thought / Action / Observation 格式写文字,程序用正则解析 Action,执行后把结果拼回去 | 第一次让模型「边想边做」 | 格式靠提示词约束,解析经常失败;参数是自由文本 |
| 2. 自主 Agent 热潮 | 2023.03–04 | AutoGPT、BabyAGI | 模型自己拆任务列表、自己循环 | 证明了「循环 + 工具」的想象力 | 打转、烧钱、不收敛,基本不可用 |
| 3. Function Calling | 2023.06 | OpenAI functions(gpt-3.5/4-0613);2023.11 改成 tools 并支持并行调用;Anthropic tool use 2024 年 GA | 工具用 JSON Schema 声明,模型输出结构化的 tool_call,由模型训练保证格式 | 解析问题消失,第一个真正可靠的 Agent 就是一个 while 循环 | 工具多了 prompt 爆炸;上下文越跑越长;没有标准接入方式 |
| 4. 框架与多 Agent | 2023–2024 | LangChain Agents、LangGraph、AutoGen、CrewAI、MetaGPT | 把流程画成图 / 角色分工 / 状态机 | 可控、可复现,适合固定业务流 | 抽象太厚、调试难;流程写死后限制强模型 |
| 5. 协议与工程化 | 2024.11–2025 | MCP(2024.11)、Anthropic《Building effective agents》(2024.12)、OpenAI Agents SDK(2025.03)、A2A(2025.04) | 工具接入标准化;区分 workflow(代码决定路径) 和 agent(模型决定路径) | 工具生态可复用;大家开始承认「简单循环 + 好工具」胜过复杂框架 | 长任务的上下文、成本、可靠性仍是难点 |
| 6. Harness / 上下文工程 | 2025–2026 | Claude Code(2025.02 预览)、Codex CLI(2025.04)、Claude Agent SDK、Agent Skills(2025.10)、推理模型交错思考 + 工具调用 | 模型外面那一圈:循环、工具、上下文管理(压缩 / 缓存 / 按需加载)、沙箱、权限、子 agent、hooks、记忆、评测 | 同一个模型,harness 好坏决定 Agent 能力上限 | 评测难、成本控制、多租户隔离、记忆治理 |
几个关键概念的一句话解释
- ReAct:Reason + Act 交替。今天的 Agent 循环本质还是 ReAct,只是「Act」从文本解析变成了结构化 tool_call,「Reason」从提示词里的 Thought 变成了模型原生的思考(reasoning / thinking)。
- Function Calling 的本质:不是模型真的调用函数,而是模型输出一段符合 schema 的 JSON,由我们的程序去执行,再把结果作为
tool消息喂回去。所以执行权、权限、超时、错误处理永远在 harness 手里。 - Workflow vs Agent(Anthropic 的划分):workflow 是代码编排 LLM(prompt chaining、routing、parallelization、orchestrator-workers、evaluator-optimizer);agent 是 LLM 自己在循环里决定下一步。能用 workflow 解决的别上 agent,路径不确定、步数不可预知的才用 agent。我们的产品里两种都有:定时任务、文档入库是 workflow;对话是 agent。
- MCP:工具的「USB 接口」。一个 MCP server 暴露 tools / resources / prompts,传输层有 stdio、SSE、Streamable HTTP。价值是工具写一次,任何支持 MCP 的 Agent 都能用。
- Skill:一个目录 +
SKILL.md(frontmatter 写名字和用途,正文写操作步骤,可以带脚本)。启动时只把名字和一句描述放进 prompt,用到时才读全文 —— 渐进式披露(progressive disclosure)。和工具的区别:工具是「一个动作」,Skill 是「一套做事的方法」,通常组合多个工具完成。 - Harness:模型以外、让模型能可靠干活的全部东西。可以类比「模型是 CPU,harness 是操作系统」。
- Context Engineering:在有限窗口里,决定每一轮让模型看到什么。包括:放什么(工具、记忆、文档)、什么时候放(按需加载)、怎么省(压缩、缓存)、怎么不污染(子 agent 隔离)。
3. 我们项目自己的演进史(和行业对得上)
| 时间 | 形态 | 做了什么 / 为什么 |
|---|---|---|
| 2025.07–2026.03 | RAG 知识库 + 框架工作流 | 文档解析流水线、向量检索、知识库 wiki;对话用 LangChain / Agno 编排几个固定 agent(文档问答、知识库问答、工作区对话、多 agent workflow) |
| 2026.04 | 自研 harness 第一版 | 4 月初写了第一版循环,叫 harness agent,几天后定名 Orchestrator;同月去掉 Agno。参考 Claude Code / Codex:单循环、工具驱动、模型自己决定 |
| 2026.05 | 配置化 + 能力扩展 | 引入统一 LLMClient,工具集合并进 profile(哪个 agent 用哪个模型、哪些工具,都在配置中心改,热更新);沙箱、子 agent(深度研究、代码执行)、SSE 改为 Redis Stream |
| 2026.06–07 | Skill 化 | 硬编码的工具(比如 html 生成器)换成 Skill,模型在沙箱里自己写代码产出文件;接入浏览器 Agent(CDP 控制用户自己的浏览器);后台任务 |
| 2026.08 | 上下文工程 | 中途插话 / 停止;prompt 分层(为了缓存);工具 deferred + ToolSearch;工具记忆(从失败里学到的经验);压缩不拆 tool_call 配对 |
| 2026.09 | 稳定性与评测 | 限流算法按模型下沉;打转守卫第二版;主 Agent 模型 benchmark;子 agent 委派边界重新设计(方案) |
| 2026.10 | 去框架依赖、补可靠性 | 去掉 litellm 只用官方 SDK;模型兜底链 + 熔断;缓存计价;沙箱 namespace 隔离;项目记忆(Context Factory) |
被问「为什么从框架换成自研」时的回答要点
- 框架的价值在模型弱的时候:要靠流程兜住模型。模型强了以后,流程变成天花板。
- Agent 循环本身很简单(核心几十行),复杂度全在边角:压缩、缓存、取消、并发、限流、计费。这些框架要么没有,要么做法和我们的业务(多租户、按组织计费、SSE 断线重放)对不上。
- 出问题时要能一眼看清模型这一轮收到了什么、发出了什么。框架多几层抽象,排查成本很高。
- 代价:所有东西要自己写和维护,比如去掉 litellm 后,OpenAI ↔ Anthropic 的流式格式转换要自己写。我们的判断是这些代码量可控,换来的是完全可控。
4. 架构总览
4.1 一次对话请求的链路
POST /workspace/completion
│ 鉴权、参数校验、创建卡片(一张卡 = 一次对话)
▼
WorkspaceService.completion()
│ ContextBuilder.build() → 拼 system prompt、历史、工具集、记忆
│ 启动后台线程跑 Orchestrator
│ 立即返回 SSE 响应(不等模型)
▼
后台:Orchestrator.run() —— 产出事件流
│ 每个事件:XADD 到 Redis Stream completion:stream:{card_id}
│ 关键节点:同步写 PostgreSQL(卡片状态、消息、运行日志)
▼
SSE 生成器:XREAD BLOCK 读 Stream → data: {...}\n\n → 浏览器同一个接口三种用法:新建(无 card_id)、重连重放(带 card_id,从 Stream 的 0-0 读全部事件)、续跑(带 resume,同一张卡追加一轮,或者回答 ask_user)。
为什么后台线程 + Redis Stream,而不是直接在请求线程里流式返回?
- 请求线程里跑:用户刷新页面 / 网络断了,生成就断了。
- 解耦后:生成和展示分离,刷新只是重新订阅;多副本下用户重连到另一个副本也能读到(状态在 Redis / PG,不在进程内)。
- 为什么不用 Celery 跑对话:多一跳 broker,首字延迟变大;对话是交互型任务,长尾的入库、wiki 同步才走 Celery。
- 进程崩了怎么办:SSE 空闲时会回查 PG 里的卡片状态,把 failed / completed 翻译成结束事件,前端不会一直转圈。
4.2 五层骨架
| 层 | 是什么 | 对应代码 |
|---|---|---|
| Agent Runtime | 循环、流式、工具分发、取消、压缩、事件封装 | core/agent/orchestrator.py、context/builder.py |
| Master | 主 agent:直接回答、调工具、决定是否委派 | AgentSpec("orchestrator") |
| Slave | 子 agent:独立上下文跑专项任务,结果折叠成一个事件返回 | tools/agent_tool.py |
| Skill | SKILL.md,按组织 > 用户 > 全局覆盖,frontmatter 控可见性 | tools/skill_tools.py、app/skills/ |
| Tool | 模型可见的最小动作:schema + handler + 元数据 | tools/registry.py 的 ToolSpec |
4.3 核心循环(面试时可以在白板上写)
def run(messages, tools):
for round in range(max_rounds):
check_cancel() # 用户点了取消?
drain_injected_input(messages) # 用户中途插话?拼进去
maybe_compact(messages) # 下一次请求要超窗口 85%?先压缩
stream = llm.chat(messages, tools=tools, stream=True)
text, tool_calls = collect(stream) # 边收边 yield 文本增量给前端
messages.append(assistant(text, tool_calls))
if not tool_calls:
break # 模型不再调工具 = 任务结束
prefetch_parallel(tool_calls) # 只读工具先并发跑起来
for call in tool_calls: # 按原顺序取结果、发事件
verdict = loop_guard(call) # 打转?软提醒 / 硬拦截
result = execute(call) if verdict != "block" else verdict.msg
messages.append(tool_result(call.id, result))
else:
yield max_rounds_reached()讲的时候强调:循环本身就这么短,难点都在每一行背后。
4.4 ToolSpec 元数据(为什么要这么多字段)
| 字段 | 作用 |
|---|---|
name / description / parameters | 发给模型的 schema。description 是模型唯一的使用说明书 |
category | 工具档位(内部知识 / 外部网络 / 写操作 …),进到模型眼里,帮助它决定先查哪 |
deferred | 默认不下发 schema,靠 ToolSearch 检索后激活 |
parallel_safe | 只读、不碰 DB session、瓶颈在网络 → 可并发 |
repeatable | 轮询类工具,同参重复是正常的,打转守卫跳过它 |
restricted_to_agent | 只给某个 agent 用(比如图谱写操作) |
interactive | 需要活人(ask_user),定时任务等无人值守场景不可用 |
4.5 简历里的设计模式落在哪(被问「具体哪里用了」时照这个答)
| 简历说法 | 代码里的实体 | 一句话 |
|---|---|---|
| 工厂模式 | core/llm/backends/registry.py 的 backend_for(protocol) | 按模型配置里的协议字段(openai / anthropic / dashscope)创建对应后端 |
| 策略模式 | core/llm/ratelimit.py 限流算法 | 百炼用滑动窗口、DeepSeek / 智谱用并发信号量,按供应商族选算法,调用方不感知 |
| 适配器模式 | anthropic_backend.py、plugins/mcp_bridge.py | Anthropic 的请求 / 流式事件转成 OpenAI 格式;MCP 工具转成内部 ToolSpec |
| 注册表模式 | tools/registry.py 的 GLOBAL_REGISTRY | 工具声明 schema 和元数据后注册,运行时按元数据调度 |
| 责任链 / 兜底链 | LLMClient + profile.config.fallbacks | 主模型重试耗尽后依次换下一个模型 |
| 熔断器 | core/llm/breaker.py | 模型连续失败后短时间直接跳过 |
| 生产者-消费者 | Orchestrator → Redis Stream → SSE | 生成和推送解耦 |
| 拦截器 / 钩子 | pre_tool_hook、打转守卫 | 工具执行前统一做检查、改参或拒绝 |
5. 关键设计逐个讲(问题 → 方案 → 取舍 → 数据)
5.1 上下文压缩
- 问题:长任务(查数、写报告)几十轮,工具结果堆起来很快超窗口。
- 方案:每轮请求前判断「下一次请求的实际大小」:上一次请求 provider 报的输入 token(准确)+ 之后新增的消息(按字符粗估)。超过窗口 85% 才压。压缩时用一个小模型把老历史总结成摘要,保留最近 8 条原文;LLM 总结失败时有确定性兜底(截断 + 去重)。
- 踩过的坑:
- 旧版用「累计输入 token 之和」判断,这个数只增不减,过了 50% 之后每一轮都压缩。压缩会改写前缀,prompt 缓存全部失效,缓存命中率从约 90% 掉到约 35%。改成「看下一次请求」后恢复。
- 压缩切点落在 assistant 的 tool_call 和对应的 tool 结果之间,下一次请求直接 400(协议要求配对)。修复:切点只落在完整的轮次边界上。
- 第 0 轮没有 usage,续跑时回放进来一大段历史,所以第 0 轮也要判,全部消息算「新增」。
- 为什么不每轮都轻压一点:每压一次就丢一次缓存。Codex 大约在 90%、Claude Code 大约 89% 才压,原则是「压缩必须少发生」。
- 为什么粗估不用分词器:跑在 gevent 单线程上,每轮对全量消息分词会卡住其他协程。粗估只用于闸门,计费用 provider 报的准确值。
5.2 Prompt 缓存
- 原理:供应商对请求的前缀做缓存,命中部分按约一折计价、首字更快。前缀里任何一个字节变了,后面全部失效。
- 方案:system prompt 按「静态前缀(全用户共享)→ 会话常量 → 每轮变化」排;工具列表顺序固定;每轮变化的东西(时间、附件、提醒)放到最后或作为增量消息追加,而不是改前面。
- 供应商差异:DeepSeek、百炼是自动缓存;Anthropic 要显式在消息块上打
cache_control断点(我们在 Anthropic 转换层里自动打)。计价也要区分缓存读 / 写 / 普通输入。 - 和压缩的关系:压缩是缓存的天敌,所以压缩阈值要高。
5.3 工具按需加载(deferred + ToolSearch)
- 问题:100+ 工具(内置工具、MCP、CLI 连接器),全量 schema 每轮都要发,又贵又让模型分心(工具越多选错越多)。
- 方案:低频工具标
deferred,只在 prompt 里有名字没有 schema;模型调ToolSearch搜到后,运行时把它的 schema 加进本次运行的工具列表(「提升」),之后几轮都可见。 - 踩过的坑:第一版只有「模型直接喊出列表外的名字」才提升。守规矩的强模型(只按列表调)永远迈不出第一步:搜到 schema 却调不动,空转了 34 分钟;反而爱猜名字的弱模型能用。修复:ToolSearch 命中就直接提升。
- 安全:提升前复核权限(属于别的 agent 的、需要活人的,不提升),否则「搜得到就用得上」会绕过权限。
- 另一个坑:ToolSearch 用子串匹配,搜不到该搜到的工具,导致看板生成很慢(后来改了匹配方式)。
5.4 并行工具调用
- 问题:模型一轮发 5 个网页抓取,串行要 5×8 秒。
- 方案:入选条件全满足才并发(只读、不碰 DB session、无 pre-hook、不是后台任务),用 gevent greenlet 提前跑起来;主循环不变,事件仍按 tool_call 原顺序发,轮到时直接取结果。不满足就退回串行,不报错。
- 细节:
- greenlet 不继承 Flask app context,每个 greenlet 要自己 push 一个(也就有了自己的 DB session),否则工具里的鉴权查库会报错。
- 用调用下标做键而不是 call_id —— 有供应商一整轮返回的 id 全是空串,用 id 做键会串结果。
5.5 打转守卫(loop guard)
- 问题:一次子 agent 打了 118 轮、468 次图谱搜索,烧了 2780 万输入 token。它每轮发 4 个调用,query 每次多拼 1–2 个词。
- 第一版为什么没拦住:只认「参数和结果字节级相同、且连续两次」。参数从来不完全一样;同一轮里穿插了别的调用,也不「连续」。
- 第二版:按工具名分桶,桶内在最近 20 次里做近似重复判定(参数 JSON 的差异比例),不要求相邻。两档阈值:差异 10% 以内出现第 3 次 → 放行但在结果后附提醒;差异 15% 以内出现第 5 次 → 直接不执行,返回强提示(硬阈值容忍度故意更宽)。轮询类工具(
repeatable)跳过。 - 取舍:调用前判断,可能误拦一次有意义的调用;但省下一次必然没新信息的真实调用,值得。
5.6 子 agent 与委派
- 为什么要子 agent:上下文隔离。让子 agent 去读 50 个网页,主 agent 只拿回一段结论,主上下文不被污染。还有并发(几个子 agent 同时查不同方向)。
- 怎么实现:子 agent 就是同一个 Orchestrator 换一份配置(系统提示、工具集、轮次上限),它的事件折叠进一个
agent事件冒泡给前端;每个子 agent 有自己的 30 分钟 watchdog,超时只杀自己,不影响父和兄弟。 - 踩过的坑 / 反思(讲这个很加分,说明有独立判断):
- 一开始限制主 agent 工具 → 什么都委派,连改一个文件也委派。
- 后来给主 agent 全部工具 → 49 轮 0 次委派,提示词里写了十几条「请委派」都没用。
- 根因不是提示词:子 agent 的工具是主 agent 的真子集,委派换不来任何新能力,还要付一次阻塞调用和重新交代上下文的成本 —— 委派是严格劣势策略;而它节省的是上下文体积,模型看不见自己的 token 账单。
- 新方案:子 agent 收敛成一种,工具继承主 agent,只保留权限性黑名单(不能再委派、不能问用户、不能改图谱);委派的判据从「难不难」换成可观察的属性(能不能并发、中间产物有没有用)。参考了 hermes-agent 的 delegate 设计。
- 为什么不搞复杂的多 agent 协作(角色扮演、互相对话):协作的 agent 之间信息传递是有损的,调试也难。目前业界做得好的(Claude Code)也是「主 agent + 隔离上下文的子任务」模式。
5.7 Tool / Skill / MCP / Plugin 怎么选
| 适合 | 我们的例子 | |
|---|---|---|
| 内置 Tool | 需要访问内部数据、要权限控制、要稳定 | 文档检索、图谱查询、数据库查数 |
| Skill | 一套做事的方法,模型在沙箱里写代码完成 | 生成 docx / pptx / xlsx / html,每个都带「自检」步骤 |
| MCP | 第三方已有的工具服务 | 企业信息查询(亿企查),支持 stdio / SSE / HTTP |
| CLI + Skill | 外部系统的读写,用命令行包一层 | 飞书、企微、CRM、自研 MES 的 CLI,Skill 教模型怎么用 |
为什么把 html 生成器从工具改成 Skill:工具是写死的模板,出来的东西千篇一律;Skill 让模型自己写代码、自己用浏览器渲染检查,质量和灵活度都更高,我们不用维护模板。
工具描述的原则(我们总结的):
- 描述是模型唯一的说明书,prompt 里不点名工具;
- 工具描述不互相引用(「优先用本地工具」这种悬空引用模型根本对不上号);
- 返回体只放结构化 JSON,不放会过期的说明文字;
- 对结果的「权重提示」(比如这个结果可能过时)拼在结果末尾,而不是塞进 system prompt。
5.8 沙箱
- 为什么需要:Skill 要模型写代码、跑代码;数据分析要跑 Python。
- 演进:先用 E2B 远程沙箱(池化,Redis + Lua 原子借还);对话主路径后来换成本机进程里的 bash 沙箱,工作目录按 (用户, 项目) 隔离,项目内多个 agent 共享产物。
- 隔离问题:所有沙箱进程是同一个 uid,用户之间能互相读文件。用
unshare建 mount / pid namespace +chroot,每个用户只看见自己的目录(没用 bwrap,是因为容器环境限制)。需要容器开SYS_ADMIN能力。 - 超时:每条命令独立超时(默认 120 秒)。
5.9 取消、停止、插话
- 取消(cancel):两条通道。卡片级(Redis 标记 + 本地标记,多副本也能收到)杀整次对话;子 agent 级只杀那一个。LLM 流的取消要强制关 socket,因为 SDK 的超时只管连接和首字节,流中间卡住不会超时。
- 停止(stop):语义是「这一轮到此结束」,落为 completed 状态,不可续跑,不算失败。用户中断单独作为一种结束原因,不再算成功或失败。
- 插话:生成过程中用户追加消息,写入队列;循环在每轮开头取出来拼进 messages,模型下一轮就能看到。
- ask_user:模型调这个工具 → 卡片进入 waiting → 用户回答后带 answers 续跑,答案回填成那次工具调用的结果。
5.10 模型接入层
- 统一 LLMClient:上层只认 OpenAI 的消息格式;Anthropic 后端负责双向转换(请求、流式事件、思考块、缓存断点)。
- 限流按供应商分族:不同供应商的限流规则本质不同 —— 百炼是滑动窗口(RPM / TPM),DeepSeek 和智谱是并发数。所以算法不同的建类层级,计价这种只是数据不同的加字段。配额桶按 api_key 指纹,而不是按模型(同一个 key 下的多个模型共享配额)。
- 按输出计量:TPM 限流要算输出 token,流式过程中边收边计,不然限流器会严重低估。
- 重试和兜底:限流退避 2/8/30 秒、瞬时故障 0.5/2/5 秒,各重试 3 次;重试完还失败,按 profile 里配的兜底链换下一个模型(层数可配);全部失败给统一提示。兜底模型用自己的原生参数,不套主模型的覆盖项(比如输出上限);压缩阈值每轮按当前模型的窗口算。
- 熔断:某模型连续失败就短时间内直接跳过,不再浪费重试时间。
- 去掉 litellm 的原因:版本锁在很老的 minor,官方只维护最近 4 个;PyPI 上发生过投毒版本;
import litellm要 1.5 秒、每个进程多占约 125MB;而且我们已经为它打了一堆补丁(参数被拦、模型名带/被当成 provider 前缀路由错)。
5.11 记忆
- 分层的判据(三个问题):会不会变?需不需要图遍历?谁写的?不变的事实、需要关系查询、系统抽取的 → 知识图谱;会变的偏好和经验、只需要读出来贴进 prompt 的 → 记忆表。
- 工具记忆:agent 从失败里学到的经验(比如某个 CLI 的某个参数怎么用),按工具挂载,调用这个工具时才把经验拼在结果后面,而不是全量注入 system prompt(我们吃过亏:一条从足浴门店报表任务学到的「查会员用 CRM」偏好,被无条件注入,后来把一次壁灯型号检索也带进了 CRM)。
- 项目记忆(Context Factory):读对话直接沉淀为项目记忆,和「治理建议」是两条并列产物。
5.12 评测
- 主 Agent 换模型前怎么知道好不好:建了几个专用 benchmark 账号(空白、有知识库、有数据源、绑了测试租户),N 条用例 × M 个模型全部真跑,在配置中心横向对比 + 人工盲评。
- 为什么不 mock 工具:飞书、企微这类工具读写混在一起,拦掉副作用就会让返回值变假,模型后面全在假世界里推理,分数看着正常其实是空的。保真度只能靠真跑换。
- 为什么要多个账号:同一账号同一时刻只能跑一格(沙箱目录、工具记忆、并发额度都是按账号共享的)。
6. 高频面试题速答
Q1:Agent 和普通的 LLM 调用、和 workflow 有什么区别? LLM 调用是一问一答;workflow 是代码决定调用顺序;agent 是模型在循环里决定下一步、什么时候结束。判断用哪个看路径是否可预知。
Q2:Function calling 的完整流程?模型真的执行了函数吗? 没有。我们把工具 schema 随请求发出;模型返回 tool_calls(name + JSON 参数);我们执行,把结果作为 role=tool、带上对应 tool_call_id 的消息追加;再请求模型。assistant 的 tool_call 和 tool 结果必须配对,否则下次请求报错。
Q3:并行 function calling 怎么处理? 一轮里模型可以返回多个 tool_call。我们对只读工具并发执行,但结果按原顺序回填。写操作串行,避免竞争。
Q4:上下文超窗口怎么办? 三层:少放(工具按需加载、文档渐进式披露:目录 → 摘要 → 全文);隔离(子 agent 吃掉中间过程);压缩(接近窗口时总结老历史,保留最近几轮,保证 tool_call 配对)。另外大的工具结果截断或落盘只给路径。
Q5:怎么降低成本? prompt 缓存(前缀稳定)是最大头;压缩少发生;工具 deferred;子 agent 隔离;打转守卫;按任务选模型(压缩走独立的 compaction profile,可以配便宜的模型)。并且所有请求记账,按组织算钱,才知道钱花在哪。
Q6:怎么防止 Agent 死循环? max_rounds 上限(到上限给提示而不是悄悄结束);打转守卫(近似重复检测);子 agent watchdog;每条 bash 命令超时;LLM 流强制关 socket。
Q7:工具多了模型选不对怎么办? 描述写清楚「什么时候用、什么时候不用」;工具档位(内部 / 外部)让模型知道先查哪里;prompt 里写检索顺序原则(先内部知识再公网);低频工具 deferred;用评测验证。我们遇到过:同一个问题,用户说了「连接企业数据库」就 11 步答对,没说就去公网搜了 8 次没找到 —— 文档其实早就入库了,问题是模型没去查,改的是 prompt 里的检索顺序和工具描述。
Q8:多 agent 你怎么看? 只在「上下文隔离」或「并发」能带来明确收益时用。委派要有能力增益或可观察收益,否则模型不会用(讲 5.6 的故事)。
Q9:MCP 是什么?和 function calling 什么关系? function calling 是模型和 harness 之间的协议;MCP 是 harness 和工具服务之间的协议。MCP server 的工具最终还是转成 function calling 的 schema 给模型。我们的 MCP 桥接把所有 MCP 工具都设成 deferred,通过 ToolSearch 暴露。
Q10:Skill 和 Tool 的区别?为什么要渐进式披露? Tool 是原子动作;Skill 是一套方法(文档 + 脚本),组合多个工具。全量放进 prompt 太长,所以只放名字和一句描述,用到时读全文。
Q11:流式输出怎么做?断线了怎么办? SSE。生成在后台,事件进 Redis Stream,SSE 从 Stream 读;断线重连从头重放;进程崩了回查 PG 状态给结束事件。为什么用 Stream 不用 Pub/Sub:Pub/Sub 不持久,断线期间的消息就丢了。
Q12:多副本部署下的状态怎么处理? 进程内不放业务状态。事件在 Redis Stream,取消标记在 Redis,卡片和消息在 PG。HAProxy 按用户做一致性哈希粘性,减少跨副本,但不依赖它保证正确性。
Q13:RAG 你们怎么做的? 解析(PDF / Office / 音频转写 / 图片理解)→ 切分 → 向量化入 Qdrant;检索固定 BM25 + 向量混合,再 rerank;按用户可见范围(个人 + 组织 + 当前项目)过滤。在 Agent 里 RAG 是一个工具(agentic RAG),模型决定什么时候检索、检索几次、换什么关键词,而不是每次对话前固定检索一次。
Q14:怎么评估 Agent 效果? 讲 5.12。补充线上指标:完成率、平均轮次、token 消耗、工具失败率、用户中断率;每张卡有完整运行日志(每一步输入输出 token、工具参数和结果)可以回放。
Q15:Agent 的安全怎么做? 权限在 harness 不在 prompt:工具按 agent 限制、提升时复核权限、图谱写操作只给特定 agent、无人值守场景禁用交互工具;沙箱 namespace 隔离;外部内容(网页、文档)只当数据。prompt 注入防不住百分之百,所以关键写操作要么需要人确认,要么不给。
Q16:如果让你从头再做一次,哪里会不同? 一开始就把「可观测」做好(每一轮请求的完整输入输出、token、缓存命中);评测更早建;更早用官方 SDK 而不是 litellm;子 agent 一开始就只做一种。
Q17:推理模型(thinking)对 Agent 有什么影响? 交错思考:模型在工具调用之间也能思考,规划能力更强。工程上要注意:思考内容要按供应商要求回传(Anthropic 的 thinking block 要原样带回);各家开启方式不同(我们按 api_base 区分写法);思考 token 要计费;有的模型开思考后不支持 temperature。
7. 踩坑故事(STAR,挑 2–3 个讲)
故事 1:缓存命中率从 90% 掉到 35%
- S:模型调用账单突然变高,看日志发现缓存命中率掉到约 35%。
- T:找出原因。
- A:对照运行日志发现每一轮都在触发压缩。压缩闸门用的是「累计输入 token 之和」,这个数只增不减,过了 50% 后每轮都满足条件。压缩会改写消息前缀,缓存全部失效。改成「上一次请求的实际输入 + 之后新增消息的估算」,阈值提到 85%。
- R:命中率回到约 90%。顺带的教训:压缩和缓存是冲突的,压缩必须少发生。
故事 2:子 agent 打转 2780 万 token
- S:一次图谱查询的子 agent 跑了 118 轮、468 次搜索。
- A:分析调用序列,发现 query 每轮多拼一两个词,同一轮里还穿插别的调用,旧守卫「字节级相同 + 连续」两个假设都不成立。改成按工具名分桶 + 窗口内近似重复判定 + 软硬两档阈值。
- R:参数(差异 15% 以内)在最近 20 次同名调用里第 3 次出现给提醒,第 5 次直接拦截,同类打转不会再烧到几十轮。
故事 3:委派 49 轮 0 次
- S:一个查数任务跑了 20 分钟,输入 token 从首轮 1.9 万涨到 9.8 万,90% 是工具结果,子 agent 一次没用上。
- A:没有继续加提示词(已经十几条了),而是分析激励:子 agent 能力是主 agent 的真子集,委派是严格劣势。方案改成单一子 agent、工具继承、只保留权限黑名单,委派判据换成可观察属性。
- R:这个是让我最有收获的一次 —— 模型行为不对时,先看结构和激励,提示词往往不是根因。
8. 反问面试官(可选)
- 你们的 Agent 是自研 harness 还是基于框架?主要的工具 / 数据接入方式是什么?
- 你们怎么评测 Agent 效果?线上主要关注哪些指标?
- 模型成本在你们的业务里占多大比重?
