88
社区成员
发帖
与我相关
我的任务
分享| 这个作业属于哪个课程 | https://bbs.csdn.net/forums/2601_CS_SE_FZU |
|---|---|
| 这个作业要求在哪里 | 软件工程实践第二次作业——与AI结对编程(顶会热词统计) |
| 这个作业的目标 | 与AI结对完成顶会热词统计平台:需求分析(NABCD)、原型设计、Web全栈实现、云部署,并完整记录人机协作过程 |
| 其他参考文献 | 《构建之法》第3、4、8章;OpenAlex API 文档;Element Plus 官方文档;ECharts 官方文档 |
@
| 项目 | 内容 |
|---|---|
| CodeArts 仓库地址 | 仓库地址 |
| 代码规范 | codestyle.md,基于 Google Python Style Guide 与 Vue.js 官方风格指南 改编 |
| 部署访问链接 | http://101.37.209.164/ |
| dev 分支 | 开发在 dev 分支进行,功能完成后合并回 main;Release 1.0.0 【请按实际仓库情况核对 commit 次数(≥15次)】 |
AI 编程助手说明
| 项目 | 说明 |
|---|---|
| 工具 | Trae(AI IDE,内置 Claude 模型) |
| 主要用途 | 需求头脑风暴、原型布局与配色讨论、代码生成与重构、Bug 调试、测试用例编写、Git 提交信息生成 |
| 协作方式 | 我负责需求判断、方案取舍、代码审查与验证;AI 负责初稿生成、方案枚举、错误排查。所有 AI 生成代码均经过我阅读理解 + pytest 测试验证后才合入 |
数据来源声明:论文数据来自 OpenAlex 开放学术 API(仅用于课程教学)。选择 OpenAlex 而非 DBLP 的原因:DBLP 存在反爬限制且缺少摘要与关键词字段,无法支撑热词统计(这一踩坑过程见第 6 节案例 C)。
| PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 25 |
| • Estimate | • 估计这个任务需要多少时间 | 30 | 25 |
| Development | 开发 | 1560 | 1890 |
| • Analysis | • 需求分析(包括学习新技术:OpenAlex API、ECharts 动画) | 120 | 150 |
| • Design Spec | • 生成设计文档(NABCD + 统计口径设计) | 60 | 70 |
| • Design Review | • 设计复审(和AI评审原型与架构) | 30 | 40 |
| • Coding Standard | • 代码规范(制定 codestyle.md) | 30 | 25 |
| • Design | • 具体设计(三层缓存、分层架构、数据库 Schema) | 90 | 120 |
| • Coding | • 具体编码(爬虫管道、5大功能、附加功能) | 900 | 1050 |
| • Code Review | • 代码复审(AI生成代码逐段审查) | 120 | 150 |
| • Test | • 测试(pytest 单元测试 103 项 + 浏览器手工测试) | 120 | 180 |
| • Test Report | • 测试报告 | 30 | 25 |
| • Size Measurement | • 计算工作量 | 20 | 15 |
| • Postmortem | • 事后总结 + 过程改进计划 | 60 | 65 |
| Development合计 | 1560 | 1890 | |
| 合计 | 1590 | 1915 |
偏差分析:实际比预估多约 20%,主要来自三处——①测试环节:关键词抽取算法迭代了 3 版(n-gram → POS 过滤 → 双重去重),每版都要回归 103 个测试;②爬虫方案试错:DBLP 方案失败后切换 OpenAlex 重写了抓取管道;③性能调优:全量热词计算首请求 26s → 三层缓存优化到秒级,超出了预期工作量。
以下分析由我从"用户、爬虫工程师、前端开发者"三个角色向 AI 提问头脑风暴,AI 输出初稿后由我逐条验证合理性、删掉伪需求(如"论文自动翻译"——超出三大顶会范围且偏离统计主线)后整理而成。
N(Need,需求)
小刚是计算机视觉领域的新人,想通过阅读论文了解研究现状。但 CVPR 2025 年投稿 13008 篇、录取 2878 篇,加上 ICCV、ECCV,仅靠人工逐篇阅读总结研究热点几乎不可行。他需要一个能自动爬取三大顶会论文、统计热门方向、并以直观可视化呈现的平台。
A(Approach,做法)
B(Benefit,好处)
C(Competitors,竞争)
D(Data,数据)
10.1109/CVPR、ECCV 各 LNCS 卷前缀)批量定位三大顶会论文;热度 = Σ(字段权重 × 词频),详见第 7 节。原型工具:墨刀(MockingBot)。
与AI协作设计的方式:
原型链接:https://1yka7mup.site.modao.ink/index.html
原型页面清单(共 7 页):
| 页面 | 对应功能 | 核心交互 |
|---|---|---|
| 首页/热门方向总览 | 功能3 | Top10 热词榜 + 年份/会议/数量筛选 + 增长率排序 |
| 关键词图谱 | 功能4 | 力导向图缩放拖拽,点击节点弹出相关论文列表 |
| 热度走势 | 功能5 | 多关键词折线 + 播放按钮逐年推进动画 |
| 年度热词演变(附加) | Bar Race | 播放/暂停/上一年/下一年/进度条跳转 |
| 论文管理 | 功能2 | 增删改查、精确/模糊搜索、分页 |
| 论文爬取 | 功能1 | 按会议+年份批量爬取 / 按标题搜索单篇,预览后勾选入库 |
| 了解更多(附加) | 顶会背景与统计口径 | 图文介绍 |
交互规则(AI 梳理数据流转后由我确认):筛选条件变化 → 触发对应 API → loading 遮罩 → 渲染图表;图谱节点点击 → 打开右侧抽屉 → 拉取该关键词下论文列表;爬取 → 预览列表(标记已存在/新增)→ 勾选入库 → 结果摘要卡片。
以下截图对应实际运行中的系统。部署链接:http://101.37.209.164/
① 首页 · Top10 热门方向榜
显示热度、论文数、增长率(▲升温/▼降温)与 2022–2026 年度热度分布迷你柱状图。可见 "diffusion models(扩散模型)" 稳居榜首,"real world" "high quality" 呈上升趋势。

② 年份/会议筛选联动
切换为"CVPR + 2024"后榜单即时刷新:"high quality(高质量)" 居首,"gaussian splatting(高斯泼溅)" 紧随其后位列第二——体现了单年单会视角下的热点差异。

③ 关键词图谱
力导向布局,节点大小表示词频,颜色按频次分 5 档(金/粉/紫/青/蓝,频次越高越暖)。节点支持拖拽与缩放。

④ 点击关键词查看相关论文
点击 "transformers" 节点,弹出该关键词下的论文列表(标题、作者、会议、年份),实现"从热点到论文"的下钻闭环。

⑤ 热度走势对比 + 播放动画
多关键词多年份折https://i-blog.csdnimg.cn/communtity/d8481c0eaf2f41dca438c67a524a49b2.jpeg "#left")
线图,点击"播放动画"按钮逐年推进绘制(按钮变为"播放中"),直观展示各方向的起落节奏。

⑥ 年度热词演变 Bar Race(附加功能)
每年独立计算 Top10 后动态竞赛排名;下图为动画播放过程中的 2023 年榜单,水平条形随排名变化实时伸缩换位。

⑦ 论文爬取页
支持"按会议批量爬取"(如 CVPR + 2024)与"按标题搜索单篇"两种模式;预览列表用标签区分"新增/已存在",避免重复入库。

⑧ 爬取预览与入库结果
勾选 全部论文确认入库后,返回新增/重复/跳过/失败的统计摘要卡片

⑨ 论文管理页
增删改查齐全,支持按编号/标题/关键词模糊搜索(可切换精确匹配)、会议年份过滤与分页浏览。

⑩ 了解更多页面(附加功能)
介绍 CVPR/ICCV/ECCV 三大顶会背景(含投稿/录用数据与本站各会议收录数),顶部以统计卡片汇总收录论文 1041 篇、关键词 3411 个,并以柱状图展示本站年度论文分布,使统计结果可解释、可复现。

⑪ 中英双语热词展示
热词榜与图谱中关键词以"英文(中文)"形式呈现,如 "semantic segmentation(语义分割)"、"diffusion models(扩散模型)",中文以紫色点缀(可见上方截图 1、3)。
本次协作中,我始终遵循"AI 出初稿 → 我验证取舍 → 测试兜底"的循环。以下 3 个案例覆盖需求设计、编码实现、调试修复三个阶段。
我的提示词:
"我在做一个 CV 三大顶会(CVPR/ICCV/ECCV)论文热词统计平台。请从终端用户、爬虫工程师、算法工程师三个角色视角,帮我头脑风暴:1) 热词统计的'热度'可以怎么定义?至少给出 4 种口径并分析利弊;2) 英文关键词抽取有哪些方案?"
AI 输出摘要:给出 4 种热度口径(纯词频 / 词频×增长率 / TF-IDF / 时间衰减加权),以及 4 种抽取方案(jieba 直接分词 / n-gram 滑窗 / TF-IDF 选词 / KeyBERT 向量召回)。
我的取舍与理由:
https://... 会严重污染热词榜。


第一轮提示词:
"用 Python 实现英文论文关键词抽取:n-gram 滑窗(1~4 gram),按标题×3/摘要×2/关键词×1 加权计分,单字词得分×0.5,输出 Top N 短语。"
AI 输出:phrases.py 完整实现。我审查后采纳了主流程,但发现两个问题:①没有做词性判断,会出现 "of the" 这类跨边界碎片和被截断的子短语;②没有考虑输入大小写归一。
迭代提示词:
"加一层 NLTK POS Tagging 白名单过滤,只保留名词/形容词/专有名词开头的短语,注意先做 tokenize + pos_tag,再按词性窗口判定短语合法性。"
AI 输出:在 _add_field() 中加入 POS 白名单。实测 Top3 抽取准确率从约 65% 提升到约 90%,"neural network" 之类的跨边界碎片基本消失。
我的后续修改(AI 未提出):增加了"包含式双重去重"——A 被 B 包含定义为"A 的 display 分词 ⊂ B 的 display 分词 或 A 的 stems 集合 ⊂ B 的 stems 集合",解决 "vision transformer" 与 "transformer" 同榜的问题。这一条 AI 第一版方案只处理了字符串包含,没考虑词干归一,被我否掉后人工改写。



Bug1:热词榜出现 "object"、"detection"、"object detection" 同榜
_dedupe_by_containment() 存在两个 bug:数据结构传错(entry["display"] 直接报 KeyError 的风险)+ 替换逻辑只吞并一个短词;{token: [phrase_indices]},只比较共享至少一个 token 的短语对,复杂度从 O(N²)(3411 个短语约 5.8M 次子集比较)降到 O(N·k),全量计算从 26s 降到约 5s;Bug2:首页年份切换出 Bug / Evolution 页面报"加载年度热词演变数据失败"
/api/evolution 返回 500,Flask 日志显示 KeyError: 'venue';{"year","phrases"} 没有 venue 字段;②即使补上 venue,get_yearly_topics() 内部还会用空 title/abstract 重新抽一遍短语——既报错又白算 2 分钟;get_yearly_topics_from_phrases() 直接复用已抽好的短语缓存。首次请求从约 120s 降到 15.6s,缓存命中后仅 167ms。Bug3:所有增长率显示 -100%
_compute_growth():末年(2026)论文仅 31 篇,多数热词在末年没出现,(0 - h0)/h0 = -100%;Bug 之外的协作记录:Element Plus 2.9.0 的 el-select 对数字类型 value 存在选中项渲染异常(文本被塞进 placeholder 元素)。AI 通过对比"venue 选择器(字符串)正常 / 年份选择器(数字)异常"定位到版本级 bug,修复方式是把全部年份 v-model 统一为字符串类型、API 调用时再 parseInt。这一类"框架暗坑"靠人肉排查可能要半天,AI 10 分钟内给出定位与修复。




graph TD
A[用户] --> B[前端 Vue3 + Element Plus + ECharts]
B --> B1[论文爬取页]
B --> B2[论文管理页]
B --> B3[首页·Top10热词榜]
B --> B4[关键词图谱]
B --> B5[热度走势]
B --> B6[年度热词演变 Bar Race]
B --> B7[了解更多]
B -->|axios /api| C[后端 Flask 分层架构]
C --> C1[路由层 api/]
C1 --> C2[服务层 analysis/]
C1 --> C3[数据层 models/ SQLAlchemy]
C2 --> C4[爬虫管道 spider/]
C4 --> C5[OpenAlex API]
C2 --> C6[短语抽取 phrases.py<br/>n-gram + POS 白名单 + 三层权重]
C2 --> C7[评分聚合 scoring.py]
C2 --> C8[去重/增长率/热词 hot_topics.py]
C2 --> C9[中英术语词典 translations.py]
C3 --> D[(SQLite<br/>1041篇论文)]
subgraph 性能保障:三级缓存
L1[一级:进程级短语抽取缓存<br/>启动时 warmup 3.4s]
L2[二级:查询结果缓存 TTL 600s]
L3[三级:Evolution 专用缓存 TTL 1800s]
end
C2 -.复用.-> L1
① 爬虫管道:数据源 → 抓取器 → 解析器 → 数据清洗器 → 数据库。清洗阶段过滤摘要中的 URL 与停用词(经验教训:否则热词榜会被 https://github.com/... 污染)。ECCV 论文分散在 Springer LNCS 多个卷中,用多个 DOI 前缀批量定位。
② 关键词抽取统计口径(平台内可查证):
热度(短语) = Σ 字段权重 × 词频 (标题=3,摘要=2,作者关键词=1)
单字词得分 × 0.5
词性白名单:仅保留 名词/形容词/专有名词
双重去重:display 分词包含 OR 词干集合包含
③ 三级缓存 + 启动预热:全量 1041 篇论文的短语抽取约 3.4s,放在 Flask 启动时 warmup_cache() 完成一次;此后所有查询都是纯内存过滤 + 聚合,配合二级/三级缓存,常规请求 <1s。这直接解决了"切换年份/会议时请求超时、Flask 线程池被占满"的线上问题。
④ 年度独立 Top N:Bar Race 页每年独立做 抽取→聚合→去重→TopN,而不是全量聚合后再按年切分——否则 2026 年(31 篇论文)的榜单会被全量高频词挤到只剩 2 条。
⑤ 质量保障:pytest 单元测试 103 项全部通过,覆盖短语抽取、去重、增长率、API 契约等核心逻辑。
def _pos_ok(ngram_tokens):
"""POS 白名单:短语必须由 名词/形容词/专有名词 构成,
且首词不能是介词/冠词/连词——消灭 'of the' 这类跨边界碎片。"""
ALLOWED = {"NN", "NNS", "NNP", "NNPS", "JJ", "JJR", "JJS"}
BANNED_HEAD = {"IN", "DT", "CC", "PRP", "TO", "VBZ", "VBP"}
tags = [t for _, t in ngram_tokens]
if not tags or tags[0] in BANNED_HEAD:
return False
# 至少含一个名词性成分(避免 "visual" 这类孤形容词)
if not any(t.startswith("NN") for t in tags):
return False
return all(t in ALLOWED for t in tags)
def _add_field(counter, text, weight):
"""对单字段做 tokenize + POS 过滤 + n-gram 计分。"""
tokens = word_tokenize(text.lower())
tagged = pos_tag(tokens)
for n in range(1, 5): # 1~4 gram 滑窗
for i in range(len(tagged) - n + 1):
window = tagged[i:i + n]
words = [w for w, _ in window]
if any(w in STOPWORDS or len(w) < 2 for w in words):
continue
if not _pos_ok(window):
continue
key = " ".join(words)
w = weight * (0.5 if n == 1 else 1.0) # 单字词降权
counter[key] = counter.get(key, 0.0) + w
设计思路:单字段内先分词并打词性标签,滑窗枚举 n-gram;先做停用词/长度硬过滤(便宜),再做 POS 白名单(较贵),把最贵的判断放最后以减少计算量。单字词 ×0.5 是因为单个词(如 "representation")信息量低于短语且噪声大。
def _dedupe_by_containment(sorted_topics):
"""输入按热度降序的 [(stem_tuple, entry), ...]。
A 被 B 吞并 = A.display 分词 ⊂ B.display 分词
或 A 的 stems ⊂ B 的 stems,
且 B 的热度 ≥ A 的 30%。
用倒排索引 {token: [indices]} 只比较共享 token 的短语对。"""
inverted = defaultdict(list)
for idx, (stems, entry) in enumerate(sorted_topics):
for tok in set(entry["display"].split()) | set(stems):
inverted[tok].append(idx)
swallowed = set()
keep = []
for idx, (stems, entry) in enumerate(sorted_topics):
if idx in swallowed:
continue
keep.append((stems, entry))
a_words = set(entry["display"].split())
a_stems = set(stems)
# 只检查排在它后面(热度更低)、且共享过 token 的候选
candidates = set()
for tok in a_words | a_stems:
candidates.update(inverted[tok])
for j in candidates:
if j <= idx or j in swallowed:
continue
stems_b, entry_b = sorted_topics[j]
b_words = set(entry_b["display"].split())
b_stems = set(stems_b)
contained = (a_words >= b_words and b_words) or \
(a_stems >= b_stems and b_stems)
if contained and entry_b["heat"] >= entry["heat"] * 0.3:
swallowed.add(j)
return keep
设计思路:榜上同时出现 "object detection" 和 "object" 时,短词是纯噪声。暴力做法是两两比较 O(N²)(3411 短语 ≈ 5.8M 次子集判定,26s);大多数短语对根本不共享任何词,所以先建倒排索引,比较范围从"全部对"缩小到"共享 token 的对",实测降到约 5s。"热度 ≥ 30%"阈值避免高频短词误吞低频长词。
def _compute_growth(per_year, first_year, last_year):
"""首年末年之间热度增长率。
首尾年份无数据时,向内侧收缩到最近一个有数据的年份再比较,
避免末年论文过少(如 2026 仅 31 篇)导致大面积 -100% 的误导。"""
if first_year == last_year:
return 0.0
h0, h1 = per_year.get(first_year, 0.0), per_year.get(last_year, 0.0)
if h0 <= 0: # 首年无数据 → 找最早有数据的年份
for y in sorted(per_year):
if first_year < y <= last_year and per_year[y] > 0:
first_year, h0 = y, per_year[y]
break
if h1 <= 0: # 末年无数据 → 找最晚有数据的年份
for y in sorted(per_year, reverse=True):
if last_year > y >= first_year and per_year[y] > 0:
last_year, h1 = y, per_year[y]
break
if first_year == last_year or h0 <= 0 or h1 <= 0:
return 0.0
return round((h1 - h0) / h0, 4)
_CACHE = {} # 二级缓存:(venue, years, top_n) → response
_CACHE_TTL = 600
_WARMUP_IN_PROGRESS = False
def _ensure_cache():
"""确保一级短语缓存就绪:run.py 启动时已 warmup(3.4s),
这里只做兜底。注意必须用 ht_mod._PAPER_PHRASES_CACHE 动态读取——
from module import X 是值拷贝,会看不到 warmup 后的更新。"""
global _WARMUP_IN_PROGRESS
if ht_mod._PAPER_PHRASES_CACHE is not None:
return ht_mod._PAPER_PHRASES_CACHE
if _WARMUP_IN_PROGRESS: # 并发请求时只等,不重复抽取
start = time.time()
while ht_mod._PAPER_PHRASES_CACHE is None and time.time() - start < 300:
time.sleep(0.5)
return ht_mod._PAPER_PHRASES_CACHE or []
_WARMUP_IN_PROGRESS = True
try:
papers = [{"title": p.title, "abstract": p.abstract or "",
"keywords": [k.name for k in p.keywords],
"year": p.year, "venue": p.venue}
for p in Paper.query.all()]
ht_mod.warmup_cache(papers)
finally:
_WARMUP_IN_PROGRESS = False
return ht_mod._PAPER_PHRASES_CACHE or []
设计思路:一级缓存是"所有论文的短语"(进程级、永不失效),二级缓存是"查询参数组合 → 响应"(TTL 10 分钟),三级是 Evolution 页专用(TTL 30 分钟)。_WARMUP_IN_PROGRESS 标志防止并发请求各自触发一次 3.4s 的全量抽取把 Flask 线程池占满——这是我们真实踩过的坑。
可解释性声明:以上代码由 AI 生成初稿、我逐段审查后修改合入(8.2 的双重判定规则、8.3 的收缩逻辑、8.4 的并发保护是我主导的修正)。全部代码我均能解释设计思路,并通过 103 项 pytest 用例验证。爬虫管道、数据库 Schema、前端 7 个页面组件由 AI 生成初稿后由我调整;中文翻译词典
translations.py约 200 条术语由我人工整理、AI 辅助校对。
从"用AI写代码"到"和AI结对"的认知转变。
接到作业时我以为"AI 结对"就是把需求丢给 AI、把代码粘出来。实际做了才发现完全不是:AI 给的初稿几乎从不能直接用——
但 AI 结对也有传统结对给不了的东西:
方法论收获:我总结出自己的协作纪律——①让 AI 先出方案清单再让它写代码(避免拿到一个"能跑但方向错"的实现);②所有 AI 代码必须先读懂再合入,读不懂的地方追问到懂;③测试兜底,103 项 pytest 是我敢于快速接受 AI 代码的底气;④关键决策(数据源、统计口径、去重规则)必须自己拍板,AI 只做参谋。
项目启动阶段对话
代码debug部分的对话
如果把 AI 看作一位真实的结对伙伴,我会这样评价它:
贡献(驾驶员/导航员角色灵活切换)
局限
一句话总结:AI 是一位"读遍天下书但没有上线经验"的伙伴——创意与广度满分,落地判断力欠费。人负责方向、验证与拍板,AI 负责初稿、枚举与陪跑,这是我验证过的最高效分工。这次结对最大的收获不是代码,而是学会了如何管理一个不会累、但会自信地说错话的同事。