大厂真题 / 蚂蚁
蚂蚁 AI Coding 8.20 真题解析:征信对账系统账单解析引擎
考试信息
- 考试日期:2026 年 8 月 20 日
- 考试类型:AI Coding 工程题
- 开发环境:云端 IDE + CodeFuse Agent
- 作答时间:约 2 小时
- 项目目录:
/home/exam
题目要求实现一套“征信对账系统——账单解析引擎”。项目目录中提供多家机构的 Excel 账单,候选人需要分析不同文件的结构,提取机构、产品和计费明细,最终输出结构化 JSON。
说明:当前留存截图只展示了 README 的前半部分,JSON 字段定义、完整验收命令和隐藏测试规则未出现在画面中。本文严格区分可确认题面与解题建议,不补造截图之外的固定字段。
一、题目要求
项目目标
征信公司内部对账系统需要对接多个数据源,数据源会定期以 Excel 文件形式发送账单。需要实现一套账单识别系统,从格式各异的账单文件中准确提取不同机构、不同产品的计费信息,并输出结构化 JSON。
背景约束
题面明确给出以下问题:
- 数据源来自多家合作机构,各机构的账单格式不统一;
- 一个账单文件可能包含多个 Sheet;
- Sheet 中可能包含汇总行、小计行、备注等非明细信息;
- 同一机构可能使用不同简称或全称;
- 部分字段可能缺失或格式不规范。
输入
指定目录下的 Excel 账单文件,格式为 .xls 或 .xlsx。
截图中可见的样例文件包括:
机构A(CODE001)202603月账单.xlsx
机构B(CODE002)202603月账单.xlsx
机构C(CODE003)202603月账单.xlsx
机构D_12月_机构D账单.xlsx
机构E-2025年12月账单.xlsx
机构F产品-机构F25年12月账单.xlsx
输出
系统需要输出账单明细 JSON 字符串。截图未展示每个 JSON 元素的完整字段定义,实际作答时必须继续阅读 README,并以题面给出的字段名、类型和输出方式为准。
二、这道题真正考什么
这不是一道“会不会调用 pandas.read_excel”的题,而是一道小型异构数据接入工程题。
1. 需求发现能力
六个文件名已经暗示:机构编码、账期、产品名和机构名可能出现在不同位置,不能只依赖统一文件名正则。候选人需要先检查所有工作簿、Sheet 名、表头和样例值,再决定抽取规则。
2. 异构数据建模
不同机构可能使用不同字段名:
产品名称、产品、业务类型;调用量、查询次数、计费数量;单价、计费单价;金额、应付金额、费用合计。
正确做法是先映射到统一内部模型,而不是把原表逐行转成字典后直接输出。
3. 鲁棒性
隐藏测试很可能包含:
- 表头不在第一行;
- 多 Sheet,部分 Sheet 不是明细表;
- 空行、合计行、备注行混入数据;
- 金额带逗号、货币符号或中文单位;
- 合并单元格导致机构名只在第一行出现;
- 数量或单价为空,但金额存在;
- 同一机构使用简称、全称或机构编码。
4. AI 协作能力
AI 可以快速生成读取、映射和测试代码,但候选人必须决定:
- 哪些规则可以通用化;
- 哪些机构需要专用适配器;
- 如何证明输出正确;
- 如何避免 AI 根据一个样例过拟合。
三、先做数据侦察,不要直接写解析器
拿到项目后,第一步应让 Agent 只做只读分析。
建议的侦察脚本
from pathlib import Path
import pandas as pd
def inspect_workbooks(root: str) -> None:
for path in sorted(Path(root).glob("*.xls*")):
print(f"\n=== {path.name} ===")
book = pd.ExcelFile(path)
print("sheets:", book.sheet_names)
for sheet in book.sheet_names:
raw = pd.read_excel(path, sheet_name=sheet, header=None, dtype=object)
print(f"\n-- {sheet}: shape={raw.shape} --")
print(raw.head(12).to_string(index=False, header=False))
if __name__ == "__main__":
inspect_workbooks(".")
这一步的目标不是产出最终结果,而是回答五个问题:
- 每个文件有哪些 Sheet?
- 真正表头位于第几行?
- 哪些列能映射到统一字段?
- 哪些行是明细,哪些行是汇总或说明?
- 机构、编码、账期和产品分别来自文件名、Sheet、表头还是数据行?
不要一开始就让 Agent “根据 README 完成全部代码”。AI 很容易只适配第一个文件,并在剩余文件上静默输出错误数据。
四、推荐架构:通用流水线 + 机构适配器
整体流程
发现 Excel 文件
-> 读取工作簿与所有 Sheet
-> 判断 Sheet 是否包含明细
-> 定位表头
-> 标准化列名
-> 识别机构与账期
-> 过滤空行、汇总行、备注行
-> 字段清洗与类型转换
-> 业务校验
-> 输出统一 JSON
-> 生成处理统计与错误信息
推荐目录
exam/
├── main.py # 程序入口
├── models.py # 统一输出模型
├── parser.py # 文件发现与解析调度
├── excel_utils.py # 表头检测、单元格清洗
├── normalizers.py # 名称、数字、日期归一化
├── adapters/
│ ├── base.py # 适配器协议
│ ├── generic.py # 通用适配器
│ └── institutions.py # 必要时存放机构专用规则
├── tests/
│ └── test_parser.py
└── README.md
考试时间有限时,可以合并文件,但逻辑层次应保留。比起文件数量,评审更关注职责是否清晰、规则是否可测试。
为什么不建议纯硬编码
下面这种代码可能通过当前样例,却很容易挂在隐藏测试:
if "机构A" in filename:
df = pd.read_excel(path, sheet_name="账单明细", skiprows=2)
elif "机构B" in filename:
df = pd.read_excel(path, sheet_name="Sheet1", skiprows=5)
更稳妥的方式是:
- 先用通用规则检测表头和字段;
- 通用规则无法识别时,再由机构适配器补充;
- 每个适配器只描述差异,不复制整套解析流程。
五、核心数据模型
实际字段必须以完整 README 为准。设计时可以先建立一个内部模型,再在输出层转换成题目要求的 JSON 键名。
from dataclasses import asdict, dataclass
from decimal import Decimal
from typing import Optional
@dataclass
class BillItem:
institution_name: str
institution_code: Optional[str]
billing_period: Optional[str]
product_name: str
quantity: Optional[int]
unit_price: Optional[Decimal]
amount: Decimal
source_file: str
source_sheet: str
source_row: int
def to_dict(self) -> dict:
data = asdict(self)
data["unit_price"] = (
str(self.unit_price) if self.unit_price is not None else None
)
data["amount"] = str(self.amount)
return data
设计原则
- 金额使用
Decimal,避免浮点误差; - 缺失字段使用
None,不要擅自填 0; - 保留
source_file、source_sheet和source_row,便于排错; - 最终序列化时再转换成 README 要求的字段与类型;
- 不要把读取 Excel 后得到的
NaN直接输出到 JSON。
六、关键实现一:表头自动定位
不同机构的表头不一定在第一行。可以读取前若干行,对每一行计算“字段别名命中数”。
HEADER_ALIASES = {
"institution_name": {"机构", "机构名称", "合作机构", "数据源"},
"institution_code": {"机构编码", "机构代码", "渠道编码"},
"product_name": {"产品", "产品名称", "业务类型", "服务名称"},
"quantity": {"数量", "调用量", "查询次数", "计费数量"},
"unit_price": {"单价", "计费单价", "含税单价"},
"amount": {"金额", "费用", "应付金额", "合计金额"},
}
def normalize_header(value: object) -> str:
if value is None:
return ""
return "".join(str(value).strip().lower().split())
def find_header_row(raw, scan_rows: int = 20) -> int | None:
aliases = {
normalize_header(alias)
for names in HEADER_ALIASES.values()
for alias in names
}
best_row = None
best_score = 0
for row_index in range(min(scan_rows, len(raw))):
values = {normalize_header(v) for v in raw.iloc[row_index].tolist()}
score = len(values & aliases)
if score > best_score:
best_row = row_index
best_score = score
return best_row if best_score >= 2 else None
这里不要追求一个“万能阈值”。先用样例验证,再决定最低命中数。若某机构表头非常特殊,可在适配器中声明自己的别名。
七、关键实现二:列名与机构名称归一化
列名映射
def build_column_mapping(columns) -> dict[str, str]:
mapping = {}
for column in columns:
normalized = normalize_header(column)
for target, aliases in HEADER_ALIASES.items():
normalized_aliases = {normalize_header(x) for x in aliases}
if normalized in normalized_aliases:
mapping[column] = target
break
return mapping
机构别名
INSTITUTION_ALIASES = {
"机构A": {"机构A", "A机构", "机构A有限公司", "CODE001"},
"机构B": {"机构B", "B机构", "机构B有限公司", "CODE002"},
}
def normalize_institution(value: str) -> str:
compact = "".join(str(value).split())
for canonical, aliases in INSTITUTION_ALIASES.items():
if compact in {"".join(x.split()) for x in aliases}:
return canonical
return compact
机构别名表应该来自对样例文件的实际检查。不要看到文件名就臆造真实企业名称,也不要在无法确认时错误合并两个机构。
八、关键实现三:过滤非明细行
只按“金额非空”判断不够,因为合计行也常有金额。
SUMMARY_WORDS = {
"合计", "总计", "小计", "本页合计", "费用合计", "备注", "说明"
}
def is_detail_row(row: dict) -> bool:
text = " ".join(
str(value).strip()
for value in row.values()
if value is not None
)
if not text:
return False
if any(word in text for word in SUMMARY_WORDS):
return False
product = row.get("product_name")
amount = row.get("amount")
quantity = row.get("quantity")
# 至少出现产品,或出现可计费的数量/金额信息。
return bool(product) and (amount is not None or quantity is not None)
更稳妥的策略是组合判断:
- 关键文本是否出现“合计”“备注”;
- 产品字段是否存在;
- 数量、单价、金额能否转成合法数值;
- 当前行是否与表头重复;
- 是否存在大量空单元格或纯说明文字。
九、关键实现四:数字和金额清洗
import re
from decimal import Decimal, InvalidOperation
def parse_decimal(value) -> Decimal | None:
if value is None:
return None
text = str(value).strip()
if not text or text.lower() in {"nan", "none", "-", "--"}:
return None
multiplier = Decimal("1")
if text.endswith("万"):
multiplier = Decimal("10000")
text = text[:-1]
text = re.sub(r"[¥¥$,,\s]", "", text)
text = text.replace("元", "")
try:
return Decimal(text) * multiplier
except InvalidOperation:
return None
金额校验
若数量、单价和金额同时存在,可以做容差校验:
from decimal import Decimal
def amount_is_consistent(quantity, unit_price, amount) -> bool:
if quantity is None or unit_price is None or amount is None:
return True
expected = Decimal(quantity) * unit_price
return abs(expected - amount) <= Decimal("0.01")
发现不一致时不要静默覆盖原金额。更好的处理是保留原值,并记录 warning 或失败原因。
十、解析主流程
from pathlib import Path
import json
import pandas as pd
def parse_sheet(path: Path, sheet_name: str) -> list[dict]:
raw = pd.read_excel(
path,
sheet_name=sheet_name,
header=None,
dtype=object,
)
header_row = find_header_row(raw)
if header_row is None:
return []
headers = raw.iloc[header_row].tolist()
table = raw.iloc[header_row + 1:].copy()
table.columns = headers
table = table.where(pd.notna(table), None)
mapping = build_column_mapping(table.columns)
table = table.rename(columns=mapping)
records = []
for offset, series in table.iterrows():
row = series.to_dict()
if not is_detail_row(row):
continue
# 这里继续执行机构、产品、账期和金额归一化,
# 再映射成 README 要求的最终字段。
row["source_file"] = path.name
row["source_sheet"] = sheet_name
row["source_row"] = int(offset) + 1
records.append(row)
return records
def parse_directory(root: str) -> list[dict]:
records = []
for path in sorted(Path(root).glob("*.xls*")):
book = pd.ExcelFile(path)
for sheet_name in book.sheet_names:
records.extend(parse_sheet(path, sheet_name))
return records
def main() -> None:
records = parse_directory(".")
print(json.dumps(records, ensure_ascii=False, allow_nan=False))
if __name__ == "__main__":
main()
这段代码是架构骨架,不是可以脱离完整 README 直接提交的最终答案。实际作答必须补上:
- 题面指定的 JSON 字段;
- 样例机构的真实别名与列名映射;
- 账期提取规则;
- 程序入口和参数格式;
- 题面要求的异常行为;
- 针对六个工作簿的回归测试。
十一、测试策略
1. 表头定位测试
- 表头位于第 1、3、8 行;
- 表头前存在标题、账期和空行;
- Sheet 完全不包含明细表头。
2. 行过滤测试
- 正常明细行;
- 空行;
- 合计、小计、备注;
- 重复表头;
- 产品名存在但金额为空;
- 金额存在但产品名为空。
3. 数字清洗测试
from decimal import Decimal
def test_parse_decimal():
assert parse_decimal("1,234.50") == Decimal("1234.50")
assert parse_decimal("¥88.00元") == Decimal("88.00")
assert parse_decimal("1.2万") == Decimal("12000.0")
assert parse_decimal("-") is None
4. 多 Sheet 测试
- 一个文件中只有一个明细 Sheet;
- 多个 Sheet 都有明细;
- 明细 Sheet 与“汇总”“说明”Sheet 混合;
- 空 Sheet 不应让整个文件失败。
5. 端到端测试
对每个样例文件至少验证:
- 解析结果不为空;
- 所有输出元素包含 README 要求的键;
- 输出是标准 JSON,不含
NaN、Infinity; - 汇总行未进入结果;
- 机构和产品完成归一化;
- 运行两次输出顺序一致;
- 单个文件失败不会阻断其他文件。
十二、两小时作答节奏
0—15 分钟:读题与盘点样例
- 通读 README,记录固定字段、输入路径、输出格式和验收命令;
- 枚举所有工作簿和 Sheet;
- 打印前 10—20 行,制作“机构—表头—字段—特殊行”清单。
15—35 分钟:确定模型与架构
- 定义统一记录模型;
- 确定表头定位、列名映射、行过滤和数值清洗策略;
- 先实现通用解析器,不做界面和复杂抽象。
35—75 分钟:打通全部样例
- 每完成一家机构就立即运行;
- 将差异限制在配置或适配器中;
- 确保所有文件都能输出,并人工抽查若干条。
75—100 分钟:补测试与异常处理
- 测空值、合计行、多 Sheet、格式错误和 JSON 合法性;
- 检查金额精度与确定性排序;
- 检查单文件失败是否会拖垮整批任务。
100—115 分钟:按 README 验收
- 运行题面给出的测试或启动命令;
- 对照输出字段逐项检查;
- 删除调试打印、硬编码绝对路径和临时文件。
115—120 分钟:交付
- 补充简短使用说明;
- 记录设计取舍与已知边界;
- 最后再执行一次完整测试,然后提交任务。
十三、可直接用于 CodeFuse 的 Prompt
Prompt 1:先分析,不写代码
请先阅读当前目录的 README.md,并检查所有 .xls/.xlsx 文件。
现在不要修改文件,也不要直接生成最终实现。
请输出:
1. README 中明确要求的输入、输出字段、程序入口和验收方式;
2. 每个工作簿的 Sheet 名、表头位置、关键列和前 5 条有效明细;
3. 各机构账单格式的共同点与差异;
4. 汇总行、小计行、备注行和空行的识别规则;
5. 一份最小可行的实现计划,以及隐藏测试最可能覆盖的边界。
所有结论必须来自当前仓库文件;无法确认的内容请标记为未知,不要猜测。
Prompt 2:实现最小闭环
根据刚才的数据盘点,实现最小可运行版本:
- 扫描题目指定目录下的 .xls/.xlsx;
- 读取所有 Sheet;
- 自动定位表头并映射到 README 指定字段;
- 过滤空行、汇总行、小计行和备注行;
- 统一机构名、产品名、账期和数值格式;
- 输出严格合法的 JSON,禁止 NaN 和 Infinity;
- 单个文件或 Sheet 解析失败时,记录错误并继续处理其他输入。
优先复用通用规则。只有样例证明通用规则无法覆盖时,才增加机构适配器。
先完成核心解析和命令行入口,不做无关界面。
完成后运行程序,并汇报每个文件、每个 Sheet 的明细条数和失败原因。
Prompt 3:针对隐藏测试审查
请审查当前实现,不要重写整个项目。重点检查:
1. 表头不在第一行;
2. 一个文件包含多个 Sheet;
3. 合并单元格产生的空机构名;
4. 合计、备注、重复表头误入明细;
5. 金额中的逗号、货币符号、中文单位和空值;
6. Decimal 精度和 JSON 中的 NaN;
7. 文件名、Sheet 名、行顺序变化后的稳定性;
8. 单文件失败是否影响整个批次;
9. 是否存在仅适配当前六个文件的硬编码;
10. 最终字段、类型和入口是否与 README 完全一致。
先列出问题和最小修复方案,再逐项修改并补测试。每次修改后运行相关测试。
Prompt 4:最终验收
请按 README 的验收标准执行最终检查:
- 运行全部测试和题目要求的命令;
- 校验输出是严格 JSON;
- 检查每条记录的必填字段和类型;
- 汇总每个输入文件、Sheet 的有效记录数;
- 抽查数量 × 单价与金额的一致性;
- 检查是否残留调试输出、绝对路径、密钥或临时文件;
- 根据项目实际代码更新使用说明,不要描述不存在的功能。
如果失败,请先定位根因,只做最小修改,直到验收通过。
十四、常见失分点
- 只解析第一个 Sheet:题面明确说明一个账单可能包含多个 Sheet。
- 默认第一行是表头:标题、账期和说明经常位于表头之前。
- 把合计行当明细:金额非空不代表它是有效产品记录。
- 直接使用
float处理金额:可能引入精度误差。 - 把缺失值全部填 0:这会混淆“真实为 0”和“无法识别”。
- 输出包含
NaN:Python 默认json.dumps可能产生非标准 JSON 值,应使用allow_nan=False。 - 完全依赖文件名识别机构:文件名规则可能变化,应结合机构编码、Sheet 和表内信息。
- 为六个文件复制六套代码:可维护性差,也容易在隐藏样例中失败。
- 没有保存来源位置:解析结果异常时无法回查原 Sheet 和行号。
- 过早让 Agent 一次性完成全部需求:缺少样例盘点和分步验收,最容易得到“看起来完整、实际不正确”的实现。
小结
这道题的稳定解法不是堆叠大量机构判断,而是建立一条清晰的数据接入流水线:
侦察样例 -> 定义统一模型 -> 自动识别表头 -> 映射字段
-> 过滤非明细 -> 清洗与校验 -> 严格 JSON 输出 -> 回归测试
AI Coding 考试中,Agent 可以帮你快速写代码,但“先观察什么、采用什么抽象、如何证明结果正确”仍然需要候选人主导。对于这道账单解析题,最重要的交付标准依次是:字段正确、样例全覆盖、异常不扩散、输出可验证、代码可扩展。