Skip to content

华为OD杭州 Python开发 - 技术面试深挖面经

字数
5931 字
阅读时间
25 分钟

场景: 技术面试官根据项目深挖实现细节 应对策略: 每一个回答都要"先说思路 → 再讲实现 → 最后说取舍"


一、项目整体架构类

Q: 先画一下你这个项目的整体架构?

"我的项目是一个前后端分离的单体应用,后端采用分层架构。从下往上分5层:

第一层:工具层 — 封装了所有具体的操作工具

  • ExcelTool:读取.xlsx/.xls文件,支持字节流
  • SQLFileTool:解析SQL文件的CREATE TABLE + INSERT语句
  • WebScraperTool:抓取网页表格和解析表单结构
  • BrowserFillerTool:Selenium控制Firefox填写表单
  • OptionMatchingTool:选项值匹配(大模型+同义词降级)
  • DataCleanerTool:DataFrame数据清洗
  • FieldMatchingTools:LangChain @tool装饰器定义的两个工具

第二层:智能体层 — Agent决策

  • MatchingAgent:字段映射Agent,调用LangChain create_agent
  • FillAgent:批量填充调度,异常重试

第三层:服务层 — 业务逻辑编排

  • DataIngestionService:统一数据接入入口
  • FieldMatchingService:映射推荐调度
  • DataFillingService:数据转换+填充执行

第四层:API层 — FastAPI路由

  • 5个核心接口:source/preview、target/preview、mapping/recommend、fill/execute、export

第五层:前端 — Vue3单页面

  • 数据上传 → 表单解析 → 字段映射 → 自动填充 → 结果导出

数据流向:前端上传源数据 → API接收 → 服务层调用工具读取+清洗 → 缓存DataFrame → 用户解析目标表单 → Agent推荐映射 → 用户确认填充 → Selenium执行 → 导出结果。"

Q: 为什么用分层架构?各层之间是怎么通信的?

"分层的原因是解耦和可维护性。我把系统分成5层,每层职责单一:

  • 工具层只做具体操作,不关心业务逻辑
  • 智能体层负责AI决策,调用工具获取信息后推理
  • 服务层负责编排流程,调用智能体和工具
  • API层负责HTTP协议处理

通信方式是依赖注入+方法调用:上层通过构造函数注入下层的实例,调用时直接走方法。比如DataIngestionService内部持有ExcelTool、SQLFileTool、WebScraperTool、DataCleanerTool四个实例,通过async方法调用。服务层内部也持有智能体实例。

这样的好处是:改某一层的实现不影响其他层,比如以后要支持JSON数据源,只需加一个JsonTool并在DataIngestionService里加一个elif分支即可。"

Q: 前端和后端是怎么交互的?用了什么协议?

"前后端通过HTTP RESTful API交互,数据格式是JSON(文件上传用multipart/form-data)。

前端技术栈是Vue3(CDN引入,没用构建工具)+ Axios做HTTP请求。 后端用FastAPI自动生成Swagger文档,前端直接按接口文档对接。

完整的交互流程:

  1. 用户选择数据源 → 前端用FormData包装文件 → POST到/api/source/preview
  2. FastAPI接收 → DataIngestionService处理 → 返回DataFrame的columns/sample/shape/dtypes
  3. 用户输入目标URL → GET /api/target/preview?target_url=xxx → 返回fields列表
  4. 前端把源数据和目标字段都传给POST /api/mapping/recommend → Agent返回映射结果
  5. 用户确认后 → POST /api/fill/execute → 返回填充结果
  6. 导出 → GET /api/export?format=excel → 返回FileResponse文件下载

跨域用了CORS中间件,当前开发环境是allow_origins=["*"]。"


二、FastAPI后端实现类

Q: 你的API是怎么设计的?有几个接口?

"一共5个核心接口,对应业务的5个步骤:

接口方法功能入参出参
/api/source/previewPOST加载源数据type+file/urlDataPreviewResponse
/api/target/previewGET解析目标表单target_urlTargetPreviewResponse
/api/mapping/recommendPOSTAI映射推荐MappingRequestMappingRecommendation
/api/fill/executePOST执行填充FillRequestFillResultResponse
/api/exportGET导出结果formatFileResponse

所有接口都用Pydantic做了数据校验和序列化。比如SourceInput定义了type/content/url三个字段,FastAPI会自动校验类型。"

Q: FastAPI的异步是怎么用的?哪些地方用了async/await?

"FastAPI基于ASGI,支持原生async/await。我在这些地方用了异步:

  1. API路由定义:所有路由都用了async def
python
@router.post("/source/preview")
async def preview_source_data(...):
    df = await ingestion_service.load_source(source_input)
  1. 服务层方法:DataIngestionService的load_source是async,因为内部可能涉及IO操作(文件读取、网络请求)

  2. Selenium调用:Selenium本身是同步阻塞的,所以我用asyncio.to_thread包装

python
async def fill_single_record(self, ...):
    return await asyncio.to_thread(self._sync_fill, ...)

这样不会阻塞事件循环,其他请求可以并发处理。

  1. 网页抓取:WebScraperTool的extract_tables用了静态解析优先,静态解析是同步的pandas.read_html。如果静态解析失败,降级到Selenium动态渲染,用asyncio.to_thread包装。

注意:FastAPI的async路由在使用同步依赖(如Selenium、pandas)时,必须用asyncio.to_thread或run_in_executor包装,否则会阻塞整个事件循环。"

Q: 数据模型是怎么设计的?用了Pydantic的哪些特性?

"数据模型在models/schemas.py中,全部继承自BaseModel。我用了Pydantic的几个核心特性:

  1. 类型注解校验:FastAPI自动校验入参类型
python
class SourceInput(BaseModel):
    type: str                    # 必须是字符串
    content: Optional[bytes]     # 可选的bytes
    url: Optional[str]           # 可选的字符串
  1. 嵌套模型:TargetFieldInfo嵌套在TargetPreviewResponse中
python
class TargetFieldInfo(BaseModel):
    label: str
    name: str
    type: str
    options: Optional[List[Dict[str, str]]] = None
  1. 响应模型:用response_model自动序列化响应
python
@router.post("/source/preview", response_model=DataPreviewResponse)
  1. 默认值:用Optional和默认值实现可选参数

Pydantic的好处是:参数错了FastAPI直接返回422 Unprocessable Entity,不用手动写校验逻辑。而且自动生成的Swagger文档会展示参数的类型和含义。"

Q: CORS是怎么配置的?有什么安全考虑?

"在main.py中配置了CORS中间件:

python
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

当前开发环境用的allow_origins=["*"],允许所有域名访问。但我知道这在生产环境有CSRF攻击风险。正确的做法是指定具体域名,比如:

python
allow_origins=["http://localhost:3000"]  # 开发环境
allow_origins=["https://yourdomain.com"]  # 生产环境

另外allow_credentials=Trueallow_origins=["*"]同时使用在某些浏览器中会被拒绝(属于安全规范),生产环境需要注意。"


三、AI/LangChain实现类

Q: 详细讲一下MatchingAgent的实现?

"MatchingAgent在agents/matching_agent.py中,核心流程是:

初始化

  1. 创建ChatOpenAI实例,配置了model、api_key、base_url、temperature=0.1(低温度保证稳定性)、max_tokens=2048
  2. 用langchain的create_agent创建Agent,绑定了两个自定义工具
  3. 定义系统提示词,明确要求Agent"先调用工具获取信息,再返回JSON"

调用流程

python
def get_mapping_recommendation(self, source_columns, source_sample, ...):
    # 1. 构建上下文prompt
    context = f"源数据表有 {len(source_columns)} 列:{', '.join(source_columns)}..."
    
    # 2. 调用Agent,设置recursion_limit=10(防止递归过深)
    result = self.agent.invoke({
        "messages": [
            {"role": "system", "content": self.system_prompt},
            {"role": "user", "content": context}
        ]
    }, config=RunnableConfig(recursion_limit=10))
    
    # 3. 从返回的messages中提取最后一条AI消息
    for msg in reversed(result["messages"]):
        if msg.type == "ai" and msg.content:
            return self._extract_json(msg.content, ...)
    
    # 4. 失败则降级
    return self._fallback_mapping(...)

关键点

  • recursion_limit设为10,因为Agent需要调用2次工具再推理,默认5层可能不够
  • 用了反向遍历找最后一条AI消息,因为Agent的messages列表是追加式的
  • 解析JSON时用了find('{')和rfind('}')提取,因为LLM可能在JSON前后加文字
  • 异常捕获后自动降级为规则匹配"

Q: 你定义的两个@tool工具具体做什么的?

"在tools/field_matching_tools.py中定义了两个工具:

analyze_source_columns

python
@tool
def analyze_source_columns(source_columns, source_sample, source_types):
    """分析源数据表的列结构"""
    # 遍历每列,拼接"列名: xxx, 类型: xxx, 示例值: val1, val2"
    # 返回文本给Agent,让LLM理解源数据的业务含义

analyze_target_fields

python
@tool
def analyze_target_fields(target_columns, target_labels, target_types, target_sample):
    """分析目标表单的字段结构"""
    # 遍历每个字段,拼接"字段名: xxx, 显示标签: xxx, 类型: xxx"
    # 返回文本给Agent

这两个工具的作用是给Agent提供详细的上下文信息。如果直接把所有列名塞给LLM,它可能不知道每个列的具体含义。通过工具调用,Agent可以看到每列的类型、前几个示例值、显示标签等,再做映射推理。

@tool装饰器的作用是把普通Python函数转换为LangChain Tool,函数的docstring就是工具的"说明书",LLM根据docstring决定何时调用此工具。"

Q: 映射失败的降级策略是怎么实现的?

"在MatchingAgent._fallback_matching方法中:

python
def _fallback_matching(self, source_columns, target_columns, target_labels):
    recommended = {}
    for tgt in target_columns:
        tgt_lower = target_labels.get(tgt, tgt).lower()
        for src in source_columns:
            if src.lower() in tgt_lower or tgt_lower in src.lower():
                recommended[tgt] = src
                break
    return {
        "recommended_mapping": recommended,
        "confidence_scores": {},
        "unmapped_source": [...],
        "unmapped_target": [...]
    }

降级逻辑是字符串包含匹配:源列名小写后检查是否包含目标字段的标签(或反之)。比如源列"姓名"和目标标签"申请人姓名"会匹配上。

这个策略虽然简单,但能处理大部分中英文匹配场景。缺点是对语义差异大的字段(如"申请人" vs "name")无能为力,这时候就需要人工手动映射了。"

Q: 选项匹配的双策略机制是怎么实现的?

"在tools/option_matching_tool.py中:

策略1:大模型直接匹配(match_value_to_options方法)

  1. 把所有选项的value和label拼成文本
  2. 用prompt让LLM判断源值应该匹配哪个选项
  3. 返回JSON格式的matched_values列表
  4. 检查返回值是否在有效选项中(防止LLM幻觉)

策略2:同义词降级匹配(_fallback_match方法)

  1. 调用_expand_synonyms_sync生成同义词列表
  2. 用同义词列表与选项逐个比对(精确匹配value或label)
  3. 支持多选场景(用split_values按逗号、顿号分割多值)

降级触发条件:

  • 没有配置DASHSCOPE_API_KEY
  • 大模型API调用失败
  • 返回的JSON解析失败
  • 返回的matched_values为空

同义词生成是调用同一个qwen-turbo模型,prompt是"请为'xxx'生成5个同义词",返回JSON数组。"


四、数据处理实现类

Q: 多源数据接入是怎么实现的?

"在services/data_ingestion.py的load_source方法中:

python
async def load_source(self, source_input):
    if source_input.type == "excel":
        df = self.excel_tool.read_from_bytes(source_input.content)
    elif source_input.type == "sql":
        sql_content = source_input.content.decode('utf-8')
        df = self.sql_tool.parse_sql_file(sql_content)
    elif source_input.type == "url":
        df = await self._read_web_table(source_input.url)
    
    df = self.cleaner.clean_dataframe(df)
    return df

三种数据源的处理:

  1. Excel:用ExcelReadTool.read_from_bytes,从字节流(io.BytesIO)读取,双引擎策略(openpyxl → xlrd降级)
  2. SQL:用SQLFileTool.parse_sql_file,5步解析(去注释 → 分割语句 → 提取表名列名 → 提取数据 → 构建DataFrame)
  3. URL:先静态解析(requests + pandas.read_html),失败后降级动态渲染(Selenium)

所有数据统一经过DataCleanerTool清洗后返回,保证输出格式一致。"

Q: Excel读取的双引擎策略是怎么实现的?

"在tools/excel_tool.py中:

python
def read_from_bytes(self, content: bytes):
    excel_file = io.BytesIO(content)  # 字节流转为类文件对象
    try:
        # 主引擎:openpyxl(支持.xlsx格式)
        df = pd.read_excel(excel_file, engine='openpyxl')
    except Exception:
        excel_file.seek(0)  # 重置指针
        try:
            # 降级引擎:xlrd(支持.xls老格式)
            df = pd.read_excel(excel_file, engine='xlrd')
        except Exception as e2:
            raise ValueError(f"无法读取Excel文件: {e2}")
    return df

设计理由:

  • openpyxl是主流引擎,支持.xlsx格式
  • xlrd支持老的.xls格式
  • 某些Excel文件可能格式不规范,双引擎策略提高兼容性
  • BytesIO可以避免把文件写到磁盘,直接从内存读取"

Q: SQL解析具体做了什么?

"在tools/sql_file_tool.py中,核心方法parse_sql_file做了5步:

Step 1: 去除注释

  • 多行注释:正则/\*.*?\*/
  • 单行注释:正则(--|#).*?(\r?\n|$)

Step 2: 分割语句

  • 按分号分割,得到独立的SQL语句列表

Step 3: 识别CREATE TABLE和INSERT

  • 用正则匹配CREATE\s+TABLEINSERT\s+INTO
  • 只取第一个CREATE TABLE,收集所有INSERT

Step 4: 解析表结构

  • 提取表名(支持反引号包裹)
  • 提取列名:定位括号区域,按逗号分割(忽略括号嵌套内的逗号)
  • 跳过约束定义(PRIMARY KEY、FOREIGN KEY等)

Step 5: 提取数据

  • 从INSERT语句的VALUES后提取行数据
  • 支持多行格式:VALUES (v1,v2),(v3,v4)
  • 正确处理引号内的逗号
  • 自动类型转换:字符串、整数、浮点、NULL

最后用pd.DataFrame组装成标准表格。"

Q: 数据清洗做了哪些步骤?

"在tools/data_cleaner_tool.py中,clean_dataframe方法有5步:

  1. 清理列名:去首尾空格 → 多空格合并为单空格 → 移除不可见字符
  2. 智能跳过表头行:检查第一行是否包含"备注/说明/note/remark"等关键词,如果超过半数单元格命中,删除该行
  3. 字符串清理:移除不可打印字符 → 多空白符合并 → 去首尾空格 → 去首尾特殊符号(*、#)
  4. 删除空行:把空字符串替换为NaN → dropna(how='all')
  5. 删除重复行:drop_duplicates() 最后reset_index重建索引。

设计要点:智能跳过表头行是为了处理Excel中常见的"备注行"问题——用户可能在数据前加了一行备注说明,需要自动识别并清理掉。"


五、Selenium浏览器自动化类

Q: 表单结构解析是怎么实现的?

"在tools/web_scraper_tool.py的_sync_extract_form_structure方法中:

  1. 启动Firefox headless模式,访问目标URL
  2. 查找所有form元素,选择包含最多input/select/textarea的form作为目标
  3. 遍历所有表单元素,对每个元素执行6优先级标签提取
    • 优先级1:通过id查找for=该id的label(最标准方式)
    • 优先级2:查找父级label包裹
    • 优先级3:aria-label属性(无障碍设计)
    • 优先级4:placeholder属性
    • 优先级5:title属性
    • 优先级6:name/id属性兜底
  4. 特殊处理select元素:提取所有option的value和label
  5. 特殊处理radio/checkbox组:按name分组,合并为一个字段(含options列表)
  6. 处理label文本清理:去掉首尾的*、:、:等标记

返回结构:{"fields": [...], "field_types": {...}, "sample": [...]}"

Q: 字段填充是怎么实现的?支持哪些字段类型?

"在tools/browser_filler_tool.py中,BrowserFillerTool支持4种字段类型:

1. 文本输入:element.clear() → element.send_keys(value) 适用:input[type=text]、input[type=number]、textarea

2. 下拉选择

python
select = Select(element)
try:
    select.select_by_visible_text(value)  # 优先按文本匹配
except:
    select.select_by_value(value)        # 降级按value匹配

3. 单选按钮

python
driver.find_element(By.XPATH, 
    f"//input[@type='radio' and @name='{field_name}' and @value='{matched_value}']")
radio.click()

如果value匹配失败,降级为label文本匹配

4. 复选框:与单选类似,支持多选(用split_values分割多值)

字段定位用了4级优先级

  1. By.NAME(最优先,因为表单字段通常有name属性)
  2. By.ID
  3. XPath placeholder包含匹配
  4. 通过label元素的text()匹配 → 获取for属性关联ID

每个字段填充失败不会中断整个流程,而是记录错误日志继续填充下一个字段。"

Q: 浏览器配置有什么讲究?

"在browser_filler_tool.py的create_firefox_driver函数中:

  1. 浏览器选择:Firefox而不是Chrome

    • 完全开源,无商业授权限制
    • Geckodriver跨平台兼容性好
    • 对自动化脚本检测相对宽松
  2. 页面加载策略:page_load_strategy = 'eager'

    • 只等待DOM加载完成,不等待所有资源(图片、CSS)
    • 比默认的normal模式更快
  3. 超时设置:page_load_timeout = 30秒

    • 防止页面加载过慢导致卡死
  4. 用户Agent:覆盖默认UA,伪装为正常浏览器

    • "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Firefox/120.0"
  5. 保持浏览器打开:keep_open = True

    • 填充完成后不关闭浏览器,用户可以检查填充结果
    • 用while True + time.sleep(1)保持,检测WebDriverException判断是否手动关闭
  6. 不加载图片/禁用推送通知:加速页面加载"

Q: 重试机制是怎么实现的?

"在BrowserFillerTool.fill_single_record中:

python
async def fill_single_record(self, target_url, data, fields_info):
    for attempt in range(self.max_retries):  # 默认2次
        try:
            return await asyncio.to_thread(self._sync_fill, ...)
        except Exception as e:
            last_error = str(e)
            await asyncio.sleep(2)  # 间隔2秒再重试
    return False, f"填充失败,已重试 {self.max_retries} 次: {last_error}"

重试策略:

  • 最多重试2次(总共尝试3次)
  • 每次间隔2秒
  • 用asyncio.to_thread包装同步的Selenium调用
  • 异常捕获保证不会中断流程

FillAgent.fill方法中还做了批量级别的异常捕获,单条记录失败不影响其他记录。"


六、前端实现类

Q: 前端是怎么和后端通信的?

"前端在fronted/main.js中,用Vue3 Composition API(setup函数)+ Axios:

  1. 状态管理:用ref和reactive管理所有状态(源数据、目标表单、映射结果、填充结果)
  2. API调用:用axios发起请求
    • 文件上传:FormData + multipart/form-data
    • GET请求:params传递参数
    • POST请求:JSON body传递参数
  3. 错误处理:try-catch + error.response?.data?.detail获取后端错误信息
  4. 加载状态:每个操作有对应的loading变量,用于UI显示loading动画
  5. 文件下载:用Blob + URL.createObjectURL实现

核心流程是5步:

  1. loadSourceData() → 加载源数据
  2. loadTargetForm() → 解析目标表单
  3. getRecommendation() → AI映射推荐
  4. confirmFill() → 确认填充执行
  5. exportData() → 导出结果"

七、设计模式与工程类

Q: 项目中用到了哪些设计模式?

"1. 策略模式:DataIngestionService.load_source根据source_input.type选择不同的读取策略(excel/sql/url) 2. 模板方法:DataCleanerTool.clean_dataframe固定了5步清洗流程,子步骤可配置 3. 装饰器模式:LangChain @tool装饰器把普通函数转为Agent可调用的工具 4. 单例模式:API路由中的ingestion_service、matching_service、filling_service都是模块级单例 5. 观察者模式:前端Vue3的ref/reactive实现响应式数据绑定 6. 代理模式:MatchingAgent作为LLM调用的代理,封装了重试、降级、异常处理 7. 工厂模式:create_firefox_driver封装了Firefox实例的创建逻辑"

Q: 如果让你改进这个系统,你会做什么?

"短期改进

  1. 引入Redis缓存替代进程内dict缓存,支持多实例部署
  2. 复用浏览器会话(当前每条记录创建新实例),提升填充效率
  3. 增加并发填充能力(用asyncio.gather并行填充多条记录)

中期改进: 4. 引入Docker容器化部署,方便环境迁移 5. 增加用户认证和请求频率限制 6. 前端框架化(用Vite + Vue3构建工具),当前用CDN引入太简陋

长期改进: 7. 字段映射引入RAG(检索增强生成),用向量数据库存储历史映射数据 8. 增加更多数据源(JSON、REST API、数据库直连) 9. 映射结果增加人工反馈机制,持续优化映射准确率 10. 引入单元测试和集成测试,当前没有测试覆盖"

Q: 你的项目有多少行代码?结构是怎样的?

"整个后端项目大约3000+行代码,分布在:

  • api/endpoints.py:~100行(API路由定义)
  • agents/matching_agent.py:~120行(Agent核心逻辑)
  • agents/fill_agent.py:~40行(批量填充调度)
  • services/data_ingestion.py:~90行(数据接入)
  • services/data_filling.py:~70行(数据填充)
  • services/field_matching.py:~35行(映射调度)
  • tools/web_scraper_tool.py:~310行(网页抓取+表单解析,最大)
  • tools/browser_filler_tool.py:~210行(浏览器填充)
  • tools/option_matching_tool.py:~150行(选项匹配)
  • tools/data_cleaner_tool.py:~165行(数据清洗)
  • tools/sql_file_tool.py:~185行(SQL解析)
  • tools/excel_tool.py:~95行(Excel读取)
  • tools/field_matching_tools.py:~60行(Agent工具定义)
  • models/schemas.py:~55行(数据模型)

前端大约300行,在fronted/main.js中。"


八、高频追问题速答

Q: 你的项目是怎么部署的?

"开发环境用uvicorn直接启动:uvicorn main:app --reload --port 8000。生产环境建议用Gunicorn+Uvicorn Workers,或者Docker容器化部署。"

Q: 你的项目有没有用数据库?

"当前版本没有用数据库,用的是进程内dict缓存。因为这是一个单机演示项目,数据量不大。生产环境建议引入Redis。"

Q: 大模型API调用超时了怎么办?

"MatchingAgent中设置了try-except捕获所有异常,超时会自动降级为规则匹配。同时RunnableConfig设置了recursion_limit=10防止递归过深。"

Q: 并发量大了怎么办?

"当前是单体架构,用FastAPI的asyncio处理IO密集型并发。如果需要更高并发,可以:

  1. 增加Uvicorn的worker数量
  2. 用Nginx做反向代理
  3. 拆分微服务(把数据处理、填充、映射拆成独立服务)"

Q: 你的项目用了哪些第三方库?

"后端依赖:fastapi、uvicorn、pandas、openpyxl、beautifulsoup4、selenium、langchain、langchain-openai、dashscope、python-dotenv、pydantic。前端没有用npm包管理,全部CDN引入。"


附录:技术深挖高频考点清单

面试官可能会追问的代码细节

模块可能的追问点
MatchingAgentcreate_agent参数?recursion_limit为什么设10?怎么提取JSON的?降级规则是什么?
WebScraperTool6优先级标签提取的具体顺序?怎么处理radio/checkbox组的?为什么用eager加载策略?
BrowserFillerTool4级字段定位的具体实现?Selenium的显式等待和隐式等待?为什么用Firefox不用Chrome?
DataCleanerTool智能跳过表头行的判断逻辑?正则表达式的具体写法?
SQLFileTool怎么处理反引号?怎么处理括号嵌套的逗号?怎么处理多行INSERT?
ExcelToolBytesIO的作用?双引擎的降级逻辑?
OptionMatchingTool同义词生成的prompt长什么样?怎么防止LLM返回无效value?
FastAPIasync和sync的混用问题?Pydantic的response_model怎么用?依赖注入怎么实现?
整体架构缓存方案?错误处理策略?扩展新数据源需要改哪些文件?

面试时的表达技巧

  1. 主动引导:讲实现时主动提"这里我用了XX设计模式/技巧",不要等面试官问
  2. 代码细节:能说出关键的行号或函数名,比如"在matching_agent.py的get_mapping_recommendation方法中"
  3. 取舍思考:每个技术决策都要说"为什么选这个 + 放弃了什么 + 后续可以怎么改"
  4. 画架构图:如果有白板,可以主动画架构图,比纯口头描述清晰10倍
  5. 演示系统:如果允许,可以现场打开项目演示,边操作边讲解

贡献者

The avatar of contributor named as freeway348 freeway348

文件历史

撰写