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 有几个原因:
- 视觉理解能力:Skyvern 用 LLM 去理解页面语义,不依赖写死的 CSS Selector 或 XPath。金融系统的前端经常改版,传统 Selector 方案维护成本极高,LLM 理解页面内容后可以自适应定位元素。
- 成熟的浏览器操作层:它已经封装了 Playwright 的操作、截图、页面等待等逻辑,我不需要重新造轮子。
- 框架可扩展:它的后端是 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:子任务的状态是怎么持久化的?为什么不放内存?
回答:
子任务状态写数据库。原因有两个:
- 断点续执:如果 Agent 进程崩溃或者服务重启,从数据库恢复 CoordinationState 就能继续执行,不需要从头来。
- 审计需求:金融合规要求每一步操作都有记录,子任务状态的变迁本身就是审计数据的一部分。
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:
- Skills Server:把 Skill 注册表暴露为 MCP Tools,LLM Agent 直接通过 MCP 协议调用技能,不需要硬编码调用逻辑。
- Approval Server:让 LLM 能查询审批状态、甚至在特定场景下自动发起审批。
- 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 通过
自动发现 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 哈希清洗具体去掉了哪些内容?为什么这么设计?
回答:
清洗的内容包括:
- 包含长数字的 id 属性:很多前端框架(React、Vue)会生成带随机数的 id,如
id="el-2847291",每次渲染不同但语义一样 data-reactid、data-testid:框架内部属性style属性:样式变化不影响操作语义class属性:同上,CSS 类名变化不改变页面功能- HTML 注释
- 空白字符归一化
设计逻辑是:保留影响 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 事件监听 三层实现:
- TenantIsolationMiddleware:每个请求进来,解析 JWT,构建 TenantContext(包含 org_id、user_id、可见部门列表、可见业务线列表、是否有全组织可见性),存入 Python 的 contextvars.ContextVar。
- ContextVar:请求作用域的线程安全上下文存储,async 环境下每个协程独立,不会串。
- SQLAlchemy 事件监听:在 task_extensions 表的所有 SELECT 查询上挂了事件监听器,自动追加过滤条件:
这样做的好处是 开发者写业务代码时不需要手动加过滤条件,框架自动保证数据隔离。漏加 WHERE 条件导致数据泄露是安全事故的高发区,用事件监听从根上堵住。
Q17:审批流的实时通知是怎么实现的?为什么用 Redis Pub/Sub 而不是轮询?
回答:
审批流程中,Executor 检测到高风险操作后需要阻塞等待审批结果。这个"等待"用 Redis Pub/Sub 实现:
- Executor 发起审批请求,写入数据库,然后 subscribe 到
approval:{approval_id}频道 - 用
asyncio.wait_for带超时地等待消息 - 审批人通过 API 做出决策后,publish 到同一个频道
- 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 的原因:
- 成本:每个操作都调 LLM 做风险分析,成本太高
- 延迟:LLM 调用至少 1-2 秒,高频操作下累积延迟显著
- 可用性: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 返回后:
clean_llm_response():去掉 markdown 代码栅栏(```json ... ```)、前后空白parse_and_validate():JSON 解析 + Pydantic 校验- 失败则重试,指数退避(1s → 2s → 4s),最多 3 次
- 每次失败的错误信息记入
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 韧性 的路径逐步深入,重点关注:
- 架构决策的合理性:为什么选这个方案,trade-off 是什么
- 金融场景的特殊要求:双控原则、合规审计、数据脱敏、降级兜底
- 对底层实现的理解:不只是会用,还要知道为什么这么设计
- 工程质量意识:测试覆盖、成本监控、容错降级
给使用这个项目的同学的建议:
- 准备好"如果重新做一次会怎么改"的回答——体现反思能力
- 对比其他方案(传统 RPA、单 Agent、简单 RBAC)的优劣,体现技术选型的判断力
查看7道真题和解析