公司未注明

AI 应用开发 / AI Agent面经合集

完整信息

24 条问法 · 24 道题

本篇目录

最近面试的次数比较多,抽取了finrpa项目的相关面经分享给大家,我面试的公司主要以中厂/Ai初创为主,对大厂选手的参考价值可能比较有限,希望能对大家有所帮助。

岗位方向:AI 应用开发 / AI Agent 方向

项目背景:基于 Skyvern 二次开发的金融级浏览器自动化平台,面向银行、保险、证券机构,覆盖多租户权限、合规审批、风控审计等企业级能力。

技术栈:Python 3.11 / FastAPI / SQLAlchemy 2.0 / PostgreSQL / Redis / React 18 / Docker Compose

一、自我介绍 & 项目介绍

Q1:简单介绍一下你自己和这个项目

回答:

我做的项目是 FinRPA Enterprise,一个面向金融机构的 AI 浏览器自动化平台,也是我做的一个对外教学性质的项目。核心思路是用 AI Agent 替代人工去操作银行、保险、证券的各类 Web 系统——比如下载银行流水、填写保单申请、提交合规报告这些重复性的操作。

项目是基于开源的 Skyvern 框架做的二次开发。Skyvern 本身是一个单机单用户的浏览器自动化工具,我在它基础上加了金融企业需要的能力:三维 RBAC 权限体系、多 Agent 协同、可组合的 Skill 库、两阶段风险检测与审批流、全链路审计、LLM 调用的韧性设计和成本控制等。

Q2:为什么选择 Skyvern 作为底座,而不是从零开始或者用 Selenium/Playwright 直接做?

回答:

选 Skyvern 有几个原因:

  1. 视觉理解能力:Skyvern 用 LLM 去理解页面语义,不依赖写死的 CSS Selector 或 XPath。金融系统的前端经常改版,传统 Selector 方案维护成本极高,LLM 理解页面内容后可以自适应定位元素。
  2. 成熟的浏览器操作层:它已经封装了 Playwright 的操作、截图、页面等待等逻辑,我不需要重新造轮子。
  3. 框架可扩展:它的后端是 FastAPI + SQLAlchemy,架构清晰,容易在上面叠加企业级模块。

但 Skyvern 的问题也很明显——单机单用户、没有权限隔离、没有审批流、没有审计、Agent 架构单一(单 Agent 顺序执行,一个步骤失败整个任务重来)。这些正好是我项目的核心工作。

二、Multi-Agent 架构深挖

Q3:你说用了多 Agent 架构,具体是怎么设计的?为什么不用单 Agent?

回答:

我设计了 Planner-Executor 双 Agent 架构,加一个 AgentCoordinator 做协调。

  • PlannerAgent:接收高层目标(比如"下载 Q1 银行流水"),调用 LLM 把它拆解成有序的子任务列表。每个子任务有明确的目标描述、完成条件、最大重试次数、失败策略(RETRY / SKIP / ABORT / REPLAN)。
  • ExecutorAgent:逐个执行子任务,每个子任务执行完汇报结果——成功还是失败、截图、当前 URL、耗时。
  • AgentCoordinator:维护整体状态(CoordinationState),在 Planner 和 Executor 之间传递消息。如果某个子任务执行失败且策略是 REPLAN,Coordinator 把已完成的子任务列表和失败信息回传给 Planner,Planner 重新规划剩余步骤。

单 Agent 的问题在于:金融操作通常是多页面、多步骤的,比如登录→搜索账户→填写日期→导出报表→下载文件,中间任何一步失败就得从头来。双 Agent 架构的好处是 Planner 可以做局部重规划——只重新规划剩余步骤而不是从头再来,这对于涉及登录态的金融系统非常关键,因为重复登录可能触发风控。

Q4:Planner 拆解出的子任务数据结构是什么样的?

回答:

用 Pydantic 定义的,核心字段:


class SubTask(BaseModel):
    subtask_id: str
    index: int                    # 执行顺序
    goal: str                     # 子任务目标,如"在日期选择器中填入 2024-01-01 到 2024-03-31"
    completion_condition: str     # 完成条件,如"页面出现'查询结果'表格"
    max_retries: int              # 单个子任务最大重试
    failure_strategy: FailureStrategy  # RETRY / SKIP / ABORT / REPLAN
    status: SubTaskStatus         # PENDING / RUNNING / SUCCESS / FAILED / SKIPPED
    result_data: dict | None
    error_message: str | None

外层还有一个 TaskPlan,包含 plan_id、navigation_goal、subtasks[]、is_replan(是否为重规划生成)、replan_reason、version。

Q5:REPLAN 有没有上限?如果 LLM 一直规划失败怎么办?

回答:

有上限,max_replans 默认是 3 次。如果超过这个次数,任务状态会转为 NEEDS_HUMAN——这是我们 LLM 韧性设计的一部分。

NEEDS_HUMAN 状态下,前端会展示当前执行到哪一步、LLM 原始输出、错误详情,运营人员可以选择:跳过当前步骤、手动执行、或者中止任务。这个选择会记录到审计日志里。

核心思想是 永远不崩溃,永远有降级路径。金融场景下一个任务执行到一半直接挂掉是不可接受的,必须有人能接管。

Q6:子任务的状态是怎么持久化的?为什么不放内存?

回答:

子任务状态写数据库。原因有两个:

  1. 断点续执:如果 Agent 进程崩溃或者服务重启,从数据库恢复 CoordinationState 就能继续执行,不需要从头来。
  2. 审计需求:金融合规要求每一步操作都有记录,子任务状态的变迁本身就是审计数据的一部分。

Coordinator 在每个子任务执行完成后更新 completed_subtasks[],同时可以挂一个 audit_callback,在子任务粒度触发审计日志写入。

三、Skill 体系深挖

Q7:你提到了可组合的 Skill 库,这个是怎么设计的?

回答:

Skill 库的设计思路是 把金融场景中反复出现的操作封装成标准化的、可复用的技能单元。

每个 Skill 继承 BaseSkill 抽象基类,必须实现:

  • skill_name、description:元信息
  • params_model:Pydantic 模型,定义该技能的输入参数和校验规则
  • error_strategy:RETRY / SKIP / ABORT,失败时的默认行为
  • execute(params, context) -> SkillResult:具体执行逻辑
  • to_audit_dict(params):返回脱敏后的参数字典,用于审计记录

注册用 @register_skill 装饰器,自动注册到全局的 SKILL_REGISTRY 字典里。发现和调用通过 get_skill(name) / list_skills()。

目前实现了 7 个技能:

LoginSkill 金融门户登录,支持验证码策略 ABORT
SessionKeepAliveSkill 长任务保活,防超时 RETRY
FormFillSkill 表单填写,支持下拉、日期选择 RETRY
SearchAndSelectSkill 搜索并选择结果 RETRY
PaginationSkill 翻页采集 SKIP
TableExtractSkill 表格解析导出 JSON/CSV RETRY
FileDownloadSkill 文件下载并校验 ABORT

Q8:Skill 和 Agent 的子任务是什么关系?是一对一吗?

回答:

不是一对一。它们是两个维度的抽象:

  • SubTask 是 Planner 从业务目标角度拆出来的逻辑步骤,比如"登录系统"、"搜索目标账户"。
  • Skill 是从技术操作角度封装的能力单元,比如 LoginSkill、FormFillSkill。

两者通过 Workflow Template 关联起来。我预置了 6 套金融行业模板(银行 2 套、保险 2 套、证券 2 套),每个模板定义了一组有序的 SkillStepDefinition——本质就是"第 N 步用哪个 Skill,参数怎么映射"。

参数映射支持两种模式:

  • 引用模式:"url": "bank_url" —— 从工作流参数中取值
  • 字面量模式:"captcha_strategy": "=skip" —— = 前缀表示直接使用该字面值

举例,银行流水下载模板:


1. LoginSkill(url → bank_url, username, password)
2. FormFillSkill(fields → {账号: account_number, 开始日期: start_date, 结束日期: end_date})
3. TableExtractSkill(format → csv)
4. FileDownloadSkill(path → output_path)

Q9:Skill 的 to_audit_dict 为什么要单独做脱敏,不直接用审计模块的脱敏?

回答:

因为 Skill 自己最清楚哪些参数是敏感的。比如 LoginSkill 知道 password 字段必须完全掩码,FormFillSkill 知道带"卡号"标签的字段要保留后四位。

审计模块(Sanitizer)做的是通用规则——基于字段类型(卡号、密码、金额)的正则匹配和掩码。Skill 层的 to_audit_dict 是第一道防线,Sanitizer 是兜底。两层脱敏确保即使 Skill 遗漏了某个敏感字段,审计层的正则也能兜住。这在金融合规场景下是必要的。

四、MCP 相关

Q10:你的项目有用到 MCP(Model Context Protocol)吗?

回答:

当前版本没有直接集成 MCP Server,但架构设计上是 MCP-Ready 的。

我的理解是,MCP 的核心价值是让 LLM 能够以标准化协议访问外部工具和数据源。在我们的场景下,可以把几个子系统暴露为 MCP Server:

  1. Skills Server:把 Skill 注册表暴露为 MCP Tools,LLM Agent 直接通过 MCP 协议调用技能,不需要硬编码调用逻辑。
  2. Approval Server:让 LLM 能查询审批状态、甚至在特定场景下自动发起审批。
  3. Audit Server:让 LLM 能检索历史操作记录,用于异常检测或问题排查。

之所以说架构是 MCP-Ready 的,是因为:

  • 所有 Skill 的输入输出都用 Pydantic 模型定义,可以直接转换为 JSON Schema 作为 MCP Tool 的参数描述。
  • 执行都是 async 的,和 MCP 的异步调用模式兼容。
  • 租户隔离用的是 ContextVar,可以在 MCP 请求边界正确传递。

Q11:你了解 MCP 的底层实现吗?它和直接的 Function Calling 有什么区别?

回答:

MCP 本质上是一个 基于 JSON-RPC 2.0 的标准化协议,定义了 LLM 和外部工具之间通信的规范。

和 Function Calling 的核心区别:

绑定关系 工具定义绑定在具体的 LLM API 调用里 工具通过独立的 MCP Server 提供,和 LLM 解耦
发现机制 每次请求手动传 tools 列表 MCP Client 通过 tools/list 自动发现 Server 上的工具
传输层 嵌在 HTTP API 请求里 支持 stdio、SSE、WebSocket 等多种传输
复用性 一次性的,换个 LLM 要重新适配 写一次 Server,任何支持 MCP 的 Client 都能用

MCP 还定义了 Resources(数据源)和 Prompts(模板)两个概念,不仅仅是工具调用。

从实现角度看,一个 MCP Server 就是一个进程,监听 JSON-RPC 请求,返回 JSON-RPC 响应。Client 端(通常是 LLM 的宿主应用)负责把 MCP Server 提供的工具列表翻译成 LLM 能理解的 Function Calling 格式,再把 LLM 返回的调用请求转发给对应的 MCP Server 执行。

五、成本控制深挖

Q12:金融 RPA 场景下 LLM 调用成本是怎么控制的?

回答:

成本控制是多层设计的,主要三个手段:

1. Action Cache(动作缓存)

对于结构相似的页面,LLM 的决策往往是相同的。我做了基于 DOM 哈希的缓存:

  • 先对 DOM 做清洗——去掉动态 ID、React 内部属性、style/class、注释、时间戳等不影响语义的部分
  • 清洗后的 DOM 做 SHA-256 哈希,再和导航目标的 MD5 拼成 cache key:action_cache:{org_id}:{dom_hash}:{goal_hash}
  • 命中缓存就直接返回之前的决策,跳过 LLM 调用
  • TTL 24 小时

预期能减少 约 60% 的 LLM 调用。比如翻页场景,每页的 DOM 结构几乎一样,第一页调 LLM,后面直接走缓存。

2. Model Router(模型路由)

不是所有页面都需要最强的模型。我做了一个基于页面复杂度评分的路由器:


PageFeatures → ComplexityLevel → ModelTier

评分因子:
  - 元素数量(>500 → 复杂)
  - iframe 嵌套深度(≥2 → 复杂)
  - Shadow DOM(→ 复杂)
  - 表单字段数(≥20 → 复杂)
  - 动态内容(AJAX/WebSocket → 中等)

路由结果:
  SIMPLE  → LIGHT 模型(如 Haiku / GPT-4o-mini),成本约 1/50
  MODERATE → STANDARD 模型(如 Sonnet)
  COMPLEX → HEAVY 模型(如 Opus / GPT-4o)

估算能节省 40-50% 的成本。

3. Dashboard 成本监控

Dashboard 会追踪各模型层级的调用次数、缓存命中率,并基于近似定价估算 USD 成本。如果缓存命中率突然下降(说明目标系统改版了),会有告警。

Q13:DOM 哈希清洗具体去掉了哪些内容?为什么这么设计?

回答:

清洗的内容包括:

  1. 包含长数字的 id 属性:很多前端框架(React、Vue)会生成带随机数的 id,如 id="el-2847291",每次渲染不同但语义一样
  2. data-reactid、data-testid:框架内部属性
  3. style 属性:样式变化不影响操作语义
  4. class 属性:同上,CSS 类名变化不改变页面功能
  5. HTML 注释
  6. 空白字符归一化

设计逻辑是:保留影响 LLM 操作决策的信息(文本内容、元素类型、层级结构),去掉不影响决策但会导致哈希变化的噪音。

这样两次页面渲染即使动态属性变了,只要功能结构没变,DOM 哈希就一致,缓存就能命中。

Q14:Model Router 的复杂度评分,你是拍脑袋定的阈值还是有数据支撑?

回答:

说实话,初始阈值是基于经验设定的——500 元素、20 表单字段这些值来自于实际观察几十个金融系统页面后的大致分界。但这个设计支持调优:

  • PageFeatures 是结构化数据,会随每次路由决策一起记录
  • Dashboard 可以回溯分析:按复杂度级别统计任务成功率,如果 SIMPLE 级别的失败率偏高,说明阈值太宽松了,应该收紧
  • 未来可以加一个反馈闭环:如果 LIGHT 模型在某个页面反复失败,自动提升该页面的复杂度评级

当前阶段用固定阈值是合理的,因为金融系统的页面复杂度分布相对稳定——要么是简单的信息展示页,要么是复杂的交易表单,中间态不多。

六、权限与审批设计

Q15:你说做了三维 RBAC,具体是哪三维?为什么不用传统的 RBAC?

回答:

三个维度:部门(Department)+ 业务线(Business Line)+ 角色(Role)。

传统 RBAC 是"用户-角色-权限"单维度的,但金融企业的组织结构天然是多维的:

  • 一个人属于"信贷部"(部门),同时参与"对公信贷"和"零售信贷"两条业务线
  • 他在信贷部是"操作员",但在跨部门协作场景下可能需要只读权限
  • 他创建的任务,只有同部门的"审批员"能审批——而且这个审批员不能同时是操作员(双控原则)

权限判定的优先级链:


1. 超管/组织管理员 → 全部权限
2. 资源的 dept_id 匹配用户所在部门 → 按部门内角色判定
3. 资源的 bl_id 匹配用户所属业务线 → 取最高部门角色
4. 用户有 CROSS_ORG_READ/APPROVE 特殊权限 → 跨部门读/审批
5. 默认 → 无权限

其中,操作员和审批员在同一部门内互斥这个约束是用数据库 CHECK 约束实现的,不是应用层校验。这样即使有 bug 绕过了业务逻辑,数据库也会拒绝违规数据。

Q16:租户隔离是怎么做的?怎么保证不会跨部门泄露数据?

回答:

用 中间件 + ContextVar + SQLAlchemy 事件监听 三层实现:

  1. TenantIsolationMiddleware:每个请求进来,解析 JWT,构建 TenantContext(包含 org_id、user_id、可见部门列表、可见业务线列表、是否有全组织可见性),存入 Python 的 contextvars.ContextVar。
  2. ContextVar:请求作用域的线程安全上下文存储,async 环境下每个协程独立,不会串。
  3. SQLAlchemy 事件监听:在 task_extensions 表的所有 SELECT 查询上挂了事件监听器,自动追加过滤条件:

这样做的好处是 开发者写业务代码时不需要手动加过滤条件,框架自动保证数据隔离。漏加 WHERE 条件导致数据泄露是安全事故的高发区,用事件监听从根上堵住。

Q17:审批流的实时通知是怎么实现的?为什么用 Redis Pub/Sub 而不是轮询?

回答:

审批流程中,Executor 检测到高风险操作后需要阻塞等待审批结果。这个"等待"用 Redis Pub/Sub 实现:

  1. Executor 发起审批请求,写入数据库,然后 subscribe 到 approval:{approval_id} 频道
  2. 用 asyncio.wait_for 带超时地等待消息
  3. 审批人通过 API 做出决策后,publish 到同一个频道
  4. Executor 收到消息,继续执行或中止

不用轮询的原因:

  • 延迟:轮询间隔太长影响响应速度,太短浪费资源。Pub/Sub 是亚毫秒级推送。
  • 资源占用:轮询需要反复查数据库,高并发下是瓶颈。Pub/Sub 是被动接收,几乎零开销。
  • 异步友好:Python asyncio 生态下,aioredis 的 subscribe 是原生非阻塞的,不会占线程。

超时处理也很关键——高风险操作 30 分钟超时,关键风险操作 60 分钟超时。超时后任务状态变为 TIMEOUT,走降级逻辑。finally 块里一定会 unsubscribe + close,防止连接泄漏。

七、风控与审计

Q18:两阶段风险检测具体是怎么运作的?为什么不直接用 LLM?

回答:

第一阶段:关键词 + 正则快速扫描

针对银行、保险、证券分别维护了领域关键词库,每个关键词标注风险等级:

  • 银行:转账、汇款、放款、提现、改密(critical/high)
  • 保险:理赔、核保、退保(high)
  • 证券:委托、融资、融券(high)
  • 正则匹配:卡号模式、超阈值金额

这一步是毫秒级的纯文本匹配,成本为零。

第二阶段:LLM 上下文分析

只有第一阶段命中了才触发。把整页上下文交给 LLM,让它判断这个操作在当前上下文下真正的风险等级。

比如页面上有"转账"两个字,但可能只是"转账记录查询"——第一阶段会命中,但 LLM 分析上下文后判定为低风险。这样能 降低误报率。

不直接用 LLM 的原因:

  1. 成本:每个操作都调 LLM 做风险分析,成本太高
  2. 延迟:LLM 调用至少 1-2 秒,高频操作下累积延迟显著
  3. 可用性:LLM 不可用时,第一阶段仍能独立工作

LLM 失败时的兜底策略是保守升级:medium → high。宁可多审批一次,不能放过一个真正的高风险操作。

Q19:全链路审计是怎么做的?怎么保证操作记录不被篡改?

回答:

审计记录的核心数据模型是 AuditLogModel,每一步浏览器操作记录以下信息:

  • 操作元信息:task_id、org_id、dept_id、bl_id、action_index、action_type(CLICK / INPUT_TEXT / SELECT / NAVIGATE / DOWNLOAD 等)
  • 操作详情:target_element(HTML 描述)、input_value(脱敏后的值)、input_value_raw_hash(原始值的 SHA-256 哈希)
  • 操作证据:screenshot_before_key、screenshot_after_key(操作前后的截图,存在 MinIO 里)
  • 审批关联:has_approval、approval_id、approver_user_id
  • 执行信息:executor(agent 还是人工)、duration_ms、execution_result

截图存储用私有部署的 MinIO,对象命名格式:audit/{org_id}/{task_id}/{action_index}_{before|after}_{uuid}.png,按月分桶(finrpa-audit-{YYYY-MM})。访问通过预签名 URL,1 小时过期。

防篡改方面:

  • 审计日志只写不改:应用层没有 UPDATE/DELETE 审计记录的接口
  • 原始值哈希:即使显示的是掩码值(如 62****5678),原始值的 SHA-256 存着,事后取证可以验证
  • 截图不可修改:MinIO 对象写入后,应用层没有覆写/删除权限
  • 时间戳 + 执行者:每条记录有精确时间和执行主体,支持操作回溯

这套设计对齐了金融监管对 操作留痕、数据可追溯、敏感信息脱敏 的要求。

八、底层实现 & 综合问题

Q20:LLM 输出不稳定怎么办?你是怎么保证结构化输出的可靠性的?

回答:

我设计了 三层韧性机制:

第一层:Prompt 级强制

在 System Prompt 里嵌入完整的 JSON Schema(从 Pydantic 模型自动生成),并用强指令约束:


"You MUST respond with valid JSON matching: {...schema...}"
"Do NOT include any text outside the JSON object."

如果模型支持 response_format(如 OpenAI 的 JSON mode),也会启用。

第二层:解析级校验

LLM 返回后:

  1. clean_llm_response():去掉 markdown 代码栅栏(```json ... ```)、前后空白
  2. parse_and_validate():JSON 解析 + Pydantic 校验
  3. 失败则重试,指数退避(1s → 2s → 4s),最多 3 次
  4. 每次失败的错误信息记入 LLMCallResult.errors[]

第三层:任务级降级

3 次都失败后,不崩溃,而是:

  • 任务状态 → NEEDS_HUMAN
  • 触发告警通知(企业微信/钉钉)
  • 前端展示 LLM 原始输出和错误详情
  • 运营人员介入处理

整个过程的结果用 LLMCallResult 统一表达:


@dataclass
class LLMCallResult:
    success: bool
    data: Any           # 解析成功的 Pydantic 实例
    raw_response: str   # LLM 原始输出
    attempts: int       # 总尝试次数
    errors: list[str]   # 每次失败的错误
    needs_human: bool   # 是否需要人工介入

Q21:整个系统的测试是怎么做的?

回答:

总共 601 个测试,企业级模块覆盖率 85%。25 个测试模块覆盖所有子系统:

  • 权限测试:权限矩阵验证、JWT 编解码、依赖注入与角色检查、多维权限解析逻辑
  • 审批测试:审批请求生命周期、Redis Pub/Sub 等待机制、审批 API 端点
  • 风控测试:两阶段风险检测、关键词命中、LLM 降级
  • 审计测试:数据脱敏、截图关联
  • Agent 测试:Planner/Executor 协调、重规划流程
  • Skill 测试:注册表发现、参数校验、错误策略
  • 缓存测试:DOM 哈希、TTL、命中/未命中
  • LLM 韧性测试:重试逻辑、NEEDS_HUMAN 转换
  • 端到端集成测试:登录 → 任务创建 → 风险检测 → 审批 → 审计 完整链路

Mock 策略:

  • Redis 用 fakeredis(内存实现,不需要 Redis 服务)
  • 异步测试用 pytest-asyncio
  • LLM 用可控的 Mock Callable
  • 数据库单元测试用 SQLite 内存库,集成测试用测试 PostgreSQL

面试总结

几场面试大都从项目介绍出发,沿着 Multi-Agent → Skills → MCP → 成本控制 → 权限审批 → 风控审计 → LLM 韧性 的路径逐步深入,重点关注:

  1. 架构决策的合理性:为什么选这个方案,trade-off 是什么
  2. 金融场景的特殊要求:双控原则、合规审计、数据脱敏、降级兜底
  3. 对底层实现的理解:不只是会用,还要知道为什么这么设计
  4. 工程质量意识:测试覆盖、成本监控、容错降级

给使用这个项目的同学的建议:

  • 准备好"如果重新做一次会怎么改"的回答——体现反思能力
  • 对比其他方案(传统 RPA、单 Agent、简单 RBAC)的优劣,体现技术选型的判断力
本篇目录
  1. 01
  2. 02
  3. 03
  4. 04
  5. 05
  6. 06
  7. 07
  8. 08
  9. 09
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24

同作者面经