1. 从一份日报说起:Agent 与 LLM 技术雷达的搭建思路
每天早上花十五分钟翻一遍 Agent 和 LLM 领域的新动态,这个习惯我坚持了快两年。最开始是手动刷各种信息源,后来信息量爆炸,手动根本看不过来,就开始琢磨怎么把这件事自动化。今天这篇内容,就是把我搭建“技术精选日报”这套流程的完整思路和实操细节拆开来讲——它本质上是一个信息聚合与筛选系统,核心目标是从海量噪音里捞出真正值得看的几条信号。
这套东西能做什么?简单说,它每天定时抓取我预设的若干信息源,经过一轮关键词过滤和相关性打分,把最值得关注的条目挑出来,按主题归类,生成一份结构化的日报。适合谁参考?如果你也在做技术跟踪、竞品调研、或者单纯想保持对某个领域的敏感度,这套思路可以直接抄作业。哪怕你不想写代码,里面关于“信息源怎么选”“筛选标准怎么定”的部分,手动操作同样适用。
我把它定位成一个个人技术雷达,而不是什么大而全的资讯平台。这个定位很关键,因为它决定了后面所有的设计取舍——不求覆盖全,只求不漏掉真正重要的东西。
2. 整体架构设计:为什么是“抓取-过滤-打分-生成”四段式
2.1 核心流程拆解与选型考量
整套流程我拆成了四段:抓取、过滤、打分、生成。为什么是这四段而不是别的分法?因为每一段解决的是一个独立的问题,段与段之间通过明确的数据结构衔接,任何一段出问题都不会污染其他段。
抓取段负责把原始内容拿回来,统一成{title, url, source, timestamp, raw_content}这样的结构。过滤段做粗筛,把明显不相关的条目扔掉,比如标题里连“Agent”“LLM”“大模型”这些词都没有的,直接淘汰。打分段是核心,给每条内容算一个相关性分数,决定它能不能进日报。生成段负责排版,把选中的条目按主题分组,输出成可读的 Markdown。
我试过把这四段揉在一起写,结果就是改一个过滤规则要动整个脚本,调试起来非常痛苦。拆开之后,每段可以独立测试、独立替换。比如后来我把打分逻辑从“关键词命中数”换成了“关键词加权+来源权重”,只改了打分段一个文件,其他部分完全没动。
提示:分段的核心原则是“每段只做一件事,输入输出格式固定”。这样即使你后面想换掉整个抓取方式,只要输出结构不变,下游完全无感。
2.2 信息源的选择逻辑与权重分配
信息源这块我踩过不少坑。一开始贪多,什么源都往里加,结果日报里全是重复内容和低质量灌水。后来我定了一个原则:每个信息源必须能回答“它比别的源多提供了什么”。
我目前用的源大致分三类。第一类是一手发布源,比如官方博客、技术团队的更新日志,这类源权威性最高,权重给到 1.0。第二类是社区讨论源,比如技术论坛的热帖、问答社区的高赞回答,这类源时效性强、视角多元,但噪音也大,权重给 0.6。第三类是聚合筛选源,就是别人已经筛过一轮的 newsletter 或者周报,权重 0.7,因为它们已经做了一轮过滤,但可能带有别人的偏好。
权重不是拍脑袋定的,我的方法是:连续跟踪两周,统计每个源产出的条目最终进入日报的比例。比例高的源说明它的内容和我关注的方向匹配度高,权重就往上调;比例低的要么降权,要么直接砍掉。这个统计过程我写了个小脚本自动跑,每周出一份源质量报告。
2.3 数据流转与存储的轻量方案
存储这块我没上数据库,直接用 JSON 文件按天存。每天一个文件,文件名就是日期,里面是一个数组,每个元素是一条处理过的条目。为什么不用数据库?因为我的数据量级很小,一天撑死几百条,JSON 完全够用,而且方便我直接用文本编辑器打开看,调试成本极低。
数据流转是这样的:抓取段把原始数据写到raw/YYYY-MM-DD.json,过滤和打分段读这个文件,处理后写到processed/YYYY-MM-DD.json,生成段读 processed 文件,输出daily/YYYY-MM-DD.md。每一段都可以单独重跑,比如我觉得今天打分有问题,改完规则直接重跑打分和生成两段就行,不用重新抓取。
这个设计还有一个好处:历史数据可回溯。我想看看三个月前某条内容当时的打分是多少,直接翻对应的 processed 文件就行。如果用了数据库,反而要多写查询代码。
3. 核心细节解析:过滤规则与打分算法的实操要点
3.1 关键词体系的搭建与维护
关键词体系是整个筛选逻辑的地基。我最开始只用了几个核心词,比如“Agent”“LLM”“大模型”,结果漏掉了很多相关但没用这些词表述的内容。后来我建了一个三层关键词结构。
第一层是核心词,就是绝对相关的词,命中就直接进入候选池。第二层是扩展词,比如“多智能体”“工具调用”“推理链”“上下文窗口”这类,命中一个加一定分数,但不直接决定入选。第三层是排除词,比如“招聘”“课程广告”“融资快讯”这类,命中直接扣分甚至淘汰。
关键词不是一成不变的。我每周会看一遍被淘汰的条目,如果发现某条其实应该入选但被漏掉了,就分析它为什么没被命中,然后补充关键词。反过来,如果某类内容频繁入选但我其实不想看,就把它加到排除词里。这个维护过程大概每周花十分钟,但效果非常明显。
注意:关键词匹配要做大小写不敏感处理,同时要考虑中英文混排的情况。比如“Agent”和“agent”要视为同一个词,“大模型”和“LLM”要根据你的实际关注方向决定是否等价。
3.2 相关性打分的计算公式与参数调优
打分公式我迭代了好几版,目前用的是这个:
score = base_score * source_weight + keyword_bonus - exclude_penalty其中base_score默认是 1.0,source_weight就是前面说的源权重,keyword_bonus是命中扩展词的累加值,每个扩展词加 0.2,exclude_penalty是命中排除词的扣分,每个扣 0.5。
这个公式看起来简单,但参数调起来有讲究。keyword_bonus的步长我试过 0.1、0.2、0.5,最后发现 0.2 最合适——太小了区分度不够,太大了几个词就能把一条低质量内容顶上去。exclude_penalty我设成 0.5,是因为我不想一棍子打死,有些内容虽然带了排除词但可能确实有价值,扣分但不直接淘汰,给它一个“将功补过”的机会。
阈值我定在 1.5。低于这个分数的条目不进日报。这个阈值也是调出来的:定 1.0 的时候日报太长,定 2.0 的时候又漏内容,1.5 是我实测下来比较平衡的点。
3.3 去重与相似内容的合并策略
去重这块我吃过亏。最开始只按 URL 去重,结果同一件事被不同源报道,日报里出现三四条,读起来很烦。后来我加了一层标题相似度去重。
具体做法是:把标题转成小写,去掉标点和停用词,然后算两个标题的 Jaccard 相似度。相似度超过 0.7 的视为同一事件,只保留分数最高的那条,同时在日报里标注“另有 N 条相关报道”。
Jaccard 相似度的计算很简单,就是两个集合的交集大小除以并集大小。比如标题 A 的词集合是 {agent, 记忆, 机制},标题 B 是 {agent, 记忆, 模块},交集是 {agent, 记忆},并集是 {agent, 记忆, 机制, 模块},相似度就是 2/4 = 0.5,不到 0.7,不合并。如果 B 是 {agent, 记忆, 机制},那相似度就是 3/3 = 1.0,合并。
这个阈值 0.7 也是试出来的。太低会误合并不同事件,太高又起不到去重效果。0.7 在我的数据上表现最好。
4. 实操过程:从零搭建一套可运行的日报系统
4.1 环境准备与依赖安装
我用的是 Python,版本 3.10 以上。依赖不多,核心就几个:
pip install requests beautifulsoup4 feedparser python-dateutilrequests用来发 HTTP 请求,beautifulsoup4用来解析 HTML,feedparser用来解析 RSS,python-dateutil用来处理时间。如果你要抓的源主要是 RSS,那feedparser基本就够了;如果是网页,就需要requests+beautifulsoup4组合。
我不建议一上来就上 Scrapy 这种重框架,除非你要抓的源特别多、特别复杂。对于个人日报这种场景,轻量脚本完全够用,而且调试起来快得多。
4.2 抓取模块的实现与容错处理
抓取模块的核心逻辑是:遍历源列表,对每个源发请求,拿到内容后统一成标准结构。这里有几个实操细节值得说。
第一,一定要设超时。我默认设 10 秒,超过就放弃这个源,记录一条错误日志,继续抓下一个。不设超时的话,某个源响应慢会把整个脚本卡死。
第二,要处理编码问题。有些源返回的编码和声明的不一致,直接解码会乱码。我的做法是先按response.encoding解,如果出现乱码特征(比如大量问号),再用response.apparent_encoding重试。
第三,要加请求头。不加的话很多源会直接拒绝。我一般加一个常见的 User-Agent,模拟正常浏览器访问。
import requests from bs4 import BeautifulSoup def fetch_source(url, timeout=10): headers = { "User-Agent": "Mozilla/5.0 (compatible; TechRadar/1.0)" } try: resp = requests.get(url, headers=headers, timeout=timeout) resp.raise_for_status() if resp.encoding and resp.encoding.lower() != "utf-8": resp.encoding = resp.apparent_encoding return resp.text except Exception as e: print(f"抓取失败: {url}, 原因: {e}") return None这段代码我用了很久,稳定性不错。关键就是那个try-except,任何异常都吞掉并记录,不让单个源的失败影响整体。
4.3 过滤与打分模块的代码实现
过滤和打分我放在一个函数里做,因为两者逻辑紧密相关。输入是原始条目列表,输出是打过分的条目列表。
CORE_KEYWORDS = ["agent", "llm", "大模型", "多智能体"] EXTEND_KEYWORDS = ["工具调用", "推理链", "上下文窗口", "记忆机制", "规划"] EXCLUDE_KEYWORDS = ["招聘", "课程", "广告", "融资快讯"] SOURCE_WEIGHTS = { "official_blog": 1.0, "community": 0.6, "newsletter": 0.7, } def score_item(item): text = (item["title"] + " " + item.get("raw_content", "")).lower() if not any(kw in text for kw in CORE_KEYWORDS): return None base = 1.0 source_weight = SOURCE_WEIGHTS.get(item["source_type"], 0.5) keyword_bonus = sum(0.2 for kw in EXTEND_KEYWORDS if kw in text) exclude_penalty = sum(0.5 for kw in EXCLUDE_KEYWORDS if kw in text) score = base * source_weight + keyword_bonus - exclude_penalty item["score"] = round(score, 2) return item if score >= 1.5 else None这段代码的逻辑很直白:先检查核心词,没有就直接扔掉;有的话算分,分数不够也扔掉。round(score, 2)是为了让分数好看一点,方便调试时对比。
4.4 日报生成与排版输出
生成模块负责把选中的条目按主题分组,输出成 Markdown。分组逻辑我用的还是关键词——如果标题里包含“记忆”“上下文”就归到“记忆与上下文”组,包含“工具”“调用”就归到“工具与调用”组,以此类推。没有命中任何分组关键词的归到“其他”。
GROUPS = { "记忆与上下文": ["记忆", "上下文", "memory", "context"], "工具与调用": ["工具", "调用", "tool", "function"], "规划与推理": ["规划", "推理", "planning", "reasoning"], } def generate_daily(items, date_str): groups = {name: [] for name in GROUPS} groups["其他"] = [] for item in sorted(items, key=lambda x: -x["score"]): placed = False for group_name, keywords in GROUPS.items(): if any(kw in item["title"].lower() for kw in keywords): groups[group_name].append(item) placed = True break if not placed: groups["其他"].append(item) lines = [f"# Agent / LLM 技术精选日报 · {date_str}\n"] for group_name, group_items in groups.items(): if not group_items: continue lines.append(f"## {group_name}\n") for item in group_items: lines.append(f"- [{item['title']}]({item['url']}) `score: {item['score']}`") lines.append("") return "\n".join(lines)输出格式我刻意保持简洁,每条就是标题加链接加分数。分数放在那里是为了让我自己知道这条为什么入选,也方便我后续调参时回溯。
5. 常见问题与排查技巧实录
5.1 抓取失败与反爬应对
抓取失败是最常见的问题。我遇到过的原因大概有这么几类:网络超时、返回 403、返回内容为空、返回内容不是预期的 HTML。
网络超时前面说了,设 timeout 就行。403 一般是请求头的问题,加上 User-Agent 基本能解决大部分。返回内容为空可能是源本身挂了,也可能是被限流了,我的做法是记录日志,连续三天都失败的源就暂时禁用,过一周再试。
还有一种情况是返回的内容是 JavaScript 渲染出来的,直接抓 HTML 拿不到数据。这种源我一般直接放弃,因为为了一个源上无头浏览器成本太高,不值得。我的原则是:如果一个源需要我花超过半小时去适配,就砍掉它。
5.2 打分偏差的调试方法
打分偏差表现为两种:该进的没进,不该进的进了。我的调试方法是每天人工抽查。
具体做法是:每天日报生成后,我花五分钟快速扫一遍,如果发现某条明显该进但没进,就去 processed 文件里找它,看它的分数是多少,为什么没到阈值。常见原因是关键词没命中,那就补关键词;或者是源权重太低,那就调权重。
反过来,如果发现某条不该进但进了,就看它为什么分数高。常见原因是命中了太多扩展词但内容其实很水,这时候要么把某些扩展词降权,要么把它加到排除词里。
这个抽查过程听起来麻烦,但每天就五分钟,坚持两周之后你的规则就会变得非常准。
5.3 日报内容同质化的破解思路
同质化是日报做久了必然遇到的问题——每天都是类似的内容,读起来没有新鲜感。我的破解思路是主动引入多样性。
具体做法有两个。一是定期轮换信息源,比如每个月换掉一个表现最差的源,换一个新的进来试试。二是给不同主题设配额,比如“记忆与上下文”最多三条,“工具与调用”最多三条,避免某个热门主题刷屏。
还有一个技巧是故意保留一些低分但视角独特的内容。比如某条内容分数只有 1.6,刚过阈值,但它的角度很新颖,我就会手动把它提到日报前面。这种人工干预不多,但能有效打破算法的惯性。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 抓取返回空 | 源挂了或被限流 | 手动访问源地址 | 记录日志,连续失败则禁用 |
| 抓取返回 403 | 缺少请求头 | 检查 headers | 添加 User-Agent |
| 内容乱码 | 编码不一致 | 检查 response.encoding | 用 apparent_encoding 重试 |
| 该进日报的没进 | 关键词未命中或分数不够 | 查 processed 文件中的分数 | 补关键词或调权重 |
| 不该进的进了 | 扩展词命中过多 | 查分数构成 | 降扩展词权重或加排除词 |
| 日报同质化 | 信息源单一或主题集中 | 看最近一周的日报 | 轮换源、设主题配额 |
| 去重不彻底 | 相似度阈值太高 | 看重复条目的相似度 | 降低阈值到 0.6 试试 |
| 脚本跑一半卡住 | 某个源响应慢 | 看日志最后一条 | 设 timeout,加异常捕获 |
这张表是我踩坑踩出来的,基本上覆盖了 90% 的日常问题。遇到新问题我会往表里加一行,慢慢就成了一本自己的运维手册。
6. 一些实操心得与后续扩展方向
这套系统我跑了快两年,最大的体会是:规则要简单,维护要勤快。我见过有人把打分逻辑写得极其复杂,几十个参数,结果调参调到崩溃。我的做法是保持公式简单,靠每天五分钟的抽查来微调,反而更稳定。
另一个心得是不要追求全自动。我一开始想做到完全无人值守,后来发现完全自动的日报质量总差那么一点。现在我保留了一个人工干预的环节——日报生成后我会快速扫一遍,手动调整几条的排序或分组。这个环节花不了几分钟,但日报的可读性提升非常明显。
后续扩展的话,我打算加一个趋势追踪功能:统计某个关键词在过去一周、一个月内的出现频率,如果频率突然上升,就在日报里加一个“趋势提醒”板块。这个功能不需要多复杂,就是简单的计数和对比,但能帮我更早发现热点。
还有一个想法是个性化推荐:根据我过去点击和阅读的行为,自动调整源权重和关键词权重。这个稍微复杂一点,需要记录我的交互数据,但逻辑上不难,就是给每个源和每个关键词维护一个动态权重,我点得多的就加权,不点的就降权。
最后分享一个小技巧:如果你不想写代码,这套逻辑用表格软件也能实现大半。把抓取到的内容贴进表格,用公式做关键词匹配和打分,用筛选功能做过滤,用排序功能做优先级排列。效果肯定不如脚本,但胜在零门槛,适合先跑通流程再考虑自动化。
我个人在实际操作中的体会是,这套东西的价值不在于技术多复杂,而在于它逼着你把“我到底关注什么”这个问题想清楚。关键词体系、源权重、分组逻辑,每一个都是在回答这个问题。想清楚了,系统自然就好用了。