Ai开发面经整理,拷打一小时汗流浃背!(附回答总结)

最近面试的次数比较多,抽取了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_idnavigation_goalsubtasks[]is_replan(是否为重规划生成)、replan_reasonversion

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_namedescription:元信息
  • 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-reactiddata-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)的优劣,体现技术选型的判断力

#AI求职实录#
全部评论
附开源项目链接: https://github.com/Musenn/finrpa-enterprise
点赞 回复 分享
发布于 昨天 17:09 山东

相关推荐

点赞 评论 收藏
分享
03-17 23:54
黑龙江大学 Java
来个白菜也好啊qaq:可以的,大厂有的缺打手
点赞 评论 收藏
分享
评论
4
7
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务