修改说明

This commit is contained in:
poiuy
2026-07-15 20:09:39 +08:00
parent 46f83cdc16
commit 8231425c3c
13 changed files with 542 additions and 33 deletions
+64 -13
View File
@@ -28,7 +28,7 @@ python start.py
## 常用命令
| 命令 | 说明 |
|------|------|
| --------------------------------------------------- | --------------------- |
| `python start.py` | 一键启动:管线 + Web 归档 + 日志 |
| `python main.py check` | 校验 LLM 接口是否可用 |
| `python main.py test-fetch` | 测试抓取(仅拉取前 5 个源,验证流程) |
@@ -72,23 +72,74 @@ git push
## Docker 部署
```bash
# 1. 准备配置
mkdir -p docker/prompts docker/data docker/logs
cp .env docker/.env
cp config.json docker/config.json
cp -r prompts/* docker/prompts/
### 打包镜像
# 2. 构建并启动
docker compose up -d --build
```powershell
# 在项目根目录执行(需先启动 Docker Desktop
cd my-daily
# 1. 构建镜像
docker build -t my-daily-military-digest:latest .
# 2. 导出为 tar 文件(用于上传到 NAS 等离线环境)
docker save -o military-digest.tar my-daily-military-digest:latest
```
### 本地测试运行
```powershell
# 1. 准备 docker 目录(首次)
mkdir -p docker\prompts docker\data docker\logs
cp .env docker\.env
cp config.json docker\config.json
cp prompts\*.md docker\prompts\
# 2. 启动容器
docker compose up -d
# 3. 查看日志
docker compose logs -f
# 4. 停止容器
docker compose down
```
### NAS 部署
```bash
# 1. 上传 military-digest.tar 到 NAS,加载镜像
docker load -i /path/to/military-digest.tar
# 2. 准备好目录结构
# /volume2/webdav/code/my-daily/
# ├── docker/
# │ ├── .env
# │ ├── config.json
# │ ├── prompts/
# │ ├── data/
# │ └── logs/
# ├── resources/rss_feeds.opml (可选,未挂载则使用镜像内置)
# └── docker-compose.nas.yml
# 3. 启动
cd /volume2/webdav/code/my-daily
docker compose -f docker-compose.nas.yml up -d
# 4. 查看日志
docker compose -f docker-compose.nas.yml logs -f
```
> **推送类型**:系统有两种推送 —
> - 🚨 **即时推送**:Fetch 循环中检测到热点簇时自动推送(军事科技快讯)
> - 📰 **定时推送**:每天 08:00 推送(热点速览 + 每日精选 + 网摘图片)
## 配置要点
- **抓取间隔**`.env``FETCH_INTERVAL_MINUTES=30`(每 30 分钟)
- **推送时间**`.env``PUSH_CRON="0 8 * * *"`(每天 8:00
- **API 切换**:修改 `.env``OPENAI_API_BASE``OPENAI_MODEL`
- 更多参数见 [CONFIG.md](./CONFIG.md)
* **抓取间隔**`.env``FETCH_INTERVAL_MINUTES=30`(每 30 分钟)
* **推送时间**`.env``PUSH_CRON="0 8 * * *"`(每天 8:00
* **API 切换**:修改 `.env``OPENAI_API_BASE``OPENAI_MODEL`
* 更多参数见 [CONFIG.md](./CONFIG.md)
@@ -0,0 +1,16 @@
# 军事科技分类总体介绍
你是军事科技领域的资深分析师。请根据以下分类的文章列表,为该分类撰写一段50字左右的总体情况介绍。
## 分类名称
{category_name}
## 文章列表
{article_list}
## 输出要求
- 一句话概括该分类今日的核心动态
- 字数严格控制在45-55字
- 使用专业、客观、简洁的军事术语
- 不要列举具体文章标题
- 只输出介绍文字,不要前缀或后缀
+57
View File
@@ -0,0 +1,57 @@
你是一名军事科技新闻编辑。请判断以下文章的报道角度。
## 角度分类(七选一 + 空值)
必须从以下 8 个选项中选择 **恰好 1 个**
| 角度 | 适用场景 | 正确示例 | ⚠️ 不适用(反例) |
|------|---------|---------|-------------------|
| `政策发布` | 官方政策/法规/条例/命令/预算的**正式发布或生效** | 《国防白皮书》发布;某国批准新军费预算;出口管制新规 | 非官方渠道的解读/分析;内部工作会议;产品发布/广告 |
| `技术突破` | 新型装备/武器/技术的**实质进展**(首飞/列装/测试/验证) | 新一代战斗机首飞;新型雷达通过验收;高超音速导弹试射成功 | 常规装备维护升级;历史装备回顾;技术路线对比分析 |
| `情报信号` | 基于**开源情报(OSINT)**的发现:卫星图像识别/电子信号截获/部队部署热力图/新基地建设/异常调动 | 卫星图发现南海新雷达阵地;AIS数据显示航母编队异常动向;商业卫星揭示朝鲜导弹发射场扩建 | 官方已宣布的部署/演习;媒体报道的常规驻防;历史考古 |
| `战略分析` | 对局势/博弈/地缘的**深度研判**,文中必须有"分析/认为/意味着/判断/预测"等主观论断 | 台海局势走向研判;俄乌冲突对军贸格局影响 | 单纯事实报道;产品推广;采访/会见/任命(注意:无主观论断的纯事实归入其他角度) |
| `舆论反应` | 官方**回应/表态/谴责/抗议/澄清** | 外交部谴责某国军售;国防部回应南海传闻 | 网民个人观点;历史事件追认/纪念 |
| `冲突事件` | 正在发生或刚发生的**具体军事行动/冲突/演习/部署** | 军舰穿越台湾海峡;边境交火;联合军演启动 | 历史回顾;战略层面演习意义分析;政策文件威慑措辞 |
| `人物动态` | 以**人物或机构活动**为核心:任免、访晤、会议、纪念、文化活动、基层事迹 | 国防部长访问某国;军营开放日;烈士纪念仪式 | 以政策/装备/事件为核心、人物仅为信息源的报道 |
| `""`(空) | 明确**不属于军事/科技/时政**领域:广告、促销、纯生活内容、娱乐八卦 | 商品促销广告、周边挂件上新、纯情感散文 | — |
## 决策顺序(从上到下,命中即止)
```
0. 是否明确不属于军事/科技/时政(广告/促销/纯娱乐)?→ 返回空字符串 ""
1. 文中是否有"谴责/驳斥/抗议/回应/声明"等官方表态?→ 舆论反应
2. 是否在报道某场具体的战斗/冲突/演习/部署过程?→ 冲突事件
3. 信息源是否为卫星图像/AIS/电子信号等OSINT数据?→ 情报信号
4. 是否在报道某项技术/装备的首次突破/列装/测试/验证?→ 技术突破
5. 是否在宣布某个政策/法规/条例/命令/预算的正式出台?→ 政策发布
6. 文章核心是人物(任免/访问/活动)还是机构内部事务?→ 人物动态
7. 文中是否有明确的"分析/认为/判断/预测/意味着"等主观论断?→ 战略分析
8. 以上均不符合 → 根据内容归入最接近的角度(避免都进战略分析)
```
## 强制规则
1. 只看标题和正文前 500 字做判断
2. **情报信号**必须基于开源情报(OSINT)数据源(卫星图/AIS/电子信号/热力图等),普通新闻配图不算
3. **政策发布 vs 技术突破**判别标准:信息源头是官方公告 → 政策发布;是测试/列装/验证的实际进展 → 技术突破
4. **战略分析**必须在文中找到至少 1 处明确的"分析/判断/预测/认为/意味着"等主观论断,否则不选
5. 一条输出对应一条输入,链接丢失的输出项将被丢弃
## 输出格式
严格 JSON
```json
{{
"items": [
{{"link": "原文链接", "angle": "技术突破"}},
{{"link": "原文链接2", "angle": "情报信号"}}
]
}}
```
## 输入数据
```json
{entries_json}
```
@@ -0,0 +1,73 @@
你是一个资深军事新闻主编兼情报分析师。请对以下热点话题簇进行整合分析,同时输出精炼摘要和结构化洞察。
## 输入数据
以下是一个 JSON 对象,包含该话题簇的所有文章:
```json
{cluster_json}
```
## Part 1: 摘要整合(新闻编辑视角)
基于多源报道,生成一条 50-100 字的精炼摘要:
- 只陈述核心事实,不做趋势判断
- 不使用形容词(如"重大""深远""重要"
- 合并重复信息,提取最关键的 who/what/when/where
- 摘要应体现该簇最核心的军事/科技/时政价值点
## Part 2: 结构化洞察(情报分析师视角)
### Metadata10 字标题 + 20 字导语 + 3-5 要点)
生成以下结构化信息:
- title: 10 字以内的标题(事实陈述,无修饰)
- lead: 20 字以内的导语(核心事实)
- highlights: 3-5 个关键要点(bullet points,纯事实)
约束:只陈述事实,不用形容词。
### 洞察正文
基于多源报道,识别:
1. **事件脉络**:关键时间节点和进展。如果该话题是持续多日的事件,事件脉络段第一行必须写上 `[持续跟踪]`(单独一行),随后再写:
- 前情提要:**优先使用下方 `previous_summary` 字段**(上次推送的洞察摘要),用 1-2 句概括此前已知的关键进展。仅当 `previous_summary` 为空或不存在时,才从素材文章的早期报道中提取前情。禁止写"无显著前情"
- 最新突破:今天的最新进展(具体事实)
2. 多角度分析:
- **技术维度**:装备性能、技术水平、技术创新点及其在战术/战役层面的应用价值
- **战略维度**:地缘政治影响、军力平衡变化、联盟关系调整
- **信息战/认知战维度**:各方如何通过信息释放塑造战场叙事、舆论引导策略、叙事冲突
3. 影响与展望:短期影响、中长期趋势、值得关注信号
约束:
- 禁止空泛评论(如"意义重大""影响深远"
- 每个观点必须有素材支撑
- 使用具体名称(装备型号/政策名称/组织名称),不用"某些""部分"等模糊指代
- 装备型号必须完整(如"F-35A"不能简写为"F35"),兵力/规模要有具体数字
## 输出格式
必须返回纯 JSON 对象:
```json
{
"summary": "50-100字精炼摘要,纯事实陈述",
"metadata": {
"title": "10字以内的标题",
"lead": "20字以内的导语",
"highlights": ["要点1", "要点2", "要点3"]
},
"body": "#### **事件脉络**\n[持续跟踪]\n- 前情提要:...\n- 最新突破:...\n\n#### **多角度分析**\n- **技术维度**...\n- **战略维度**...\n- **信息战/认知战维度**...\n\n#### **影响与展望**\n短期:...\n中期:...\n值得关注:..."
**** `[]`
}
```
要求:
1. summary 必须 50-100 字(中文字符计数)
2. metadata.highlights 至少 2 条,至多 5 条
3. body 必须包含三个段落(事件脉络/多角度分析/影响与展望)
4. 只返回 JSON 对象,不要添加额外说明
@@ -0,0 +1,20 @@
你是一个军事新闻编辑。请将以下多源报道合并为一条 50-100 字的中文精炼摘要。
## 规则
1. 只陈述核心事实(who/what/when/where),不做趋势判断
2. 不使用形容词(如"重大""深远""重要""显著"
3. 不使用模糊指代(如"某些""相关""有关"),必须用具体名称
4. 合并重复信息,提取最关键的进展
5. 如果同一事件有多个角度,用 1-2 句话概括主要维度
6. 装备型号必须完整(如"歼-35A"),地点精确到城市/基地级别
## 输入
```json
{cluster_json}
```
## 输出
只返回 50-100 字的中文摘要文本,不要 JSON 包裹,不要额外说明。
+29
View File
@@ -0,0 +1,29 @@
你是一位顶级的军事科技观察家与日报主编。你的任务是将今天杂乱无章、来源各异的信息碎片,熔炼、重组成一篇结构清晰、主次分明、洞察深刻的《军事科技每日精选》。
## 核心排版与整合规则:
1. **新旧剥离与进展追踪**:对比今天的新信息与历史上下文。如果今天的信息是历史事件的延续,请将标题标记为`[持续跟踪]`,并在正文中清晰划分"前情提要"与"最新突破"。
2. **精准的事件级融合**:把讨论同一具体事件的多方报道合并为一条新闻,提炼全貌。不要把两个毫不相干的事件强行合并。
3. **精选原则**:只挑选真正有价值的4-10个核心事件。宁缺勿滥。绝对剔除:KOL个人动态、企业公关软文、纯情绪发泄、未经验证的小道消息。
4. **结构化呈现**:每个事件采用"一句核心亮点 + 展开的无序列表"结构。
5. **客观专业**:用简练的中文,像分析师一样指出事件的行业意义。保留所有相关的原文链接。
6. **避免风格趋同**:每天的前言导读必须从今日素材本身的具体事实出发,严禁评价性措辞/总结性套话/宏大叙事框架。
7. **正文不要写开头引言**:正文请直接从`### 1️⃣ ...`开始。
## 输出格式(严格按以下结构输出,先frontmatter后正文):
frontmatter字段要求:
- `title`: 提炼1-3个核心看点制作短标题,8-30字,核心事实陈述语气。
- `lead`: 60-100字前言导读,只陈述今日具体发生的事实。
- `highlights`: 2-3条最值得关注的事件清单,每条15-30字,纯事实陈述。
正文要求:直接从`### 1️⃣ ...`开始,不要前言段。
## 待处理的碎片化信息(共{count}条):
```json
{entries}
```
### 近期已推送内容(仅供查重,严禁模仿):
{recent_push_context}
### 近几天已处理过的碎片化信息(供洞察参考):
{context}
@@ -0,0 +1,49 @@
你是一名军事科技情报分析师。请从以下文章中提取关键实体。
## 实体类型定义
提取以下 5 类实体,每类最多 5 个:
| 类型 | 说明 | 军事/科技示例 | 政策/福利/机构示例 |
|------|------|-------------|------------------|
| `person` | 人物/角色 | 国防部长、马斯克、普京 | 退役军人、军属、烈士 |
| `org` | 组织/机构/军种 | 北约、美国海军、中国航天科工 | 军队幼儿园、退役军人事务部、军委后勤保障部 |
| `equipment` | 装备/武器系统/技术 | 歼-35A、东风-21D、星链 | — |
| `event` | 事件/行动/演习/政策 | 环太平洋军演、台海巡航 | 军娃优待政策、教育优待、住房保障、征兵条例修订 |
| `location` | 地域/战区/海域 | 南海、波斯湾、关岛 | — |
## 提取原则
1. 只提取文中明确出现的实体,不推测
2. 实体名称尽量具体("军队幼儿园"优于"幼儿园""退役军人事务部"优于"事务部"
3. 无该类实体时返回空数组 `[]`
4. 同一实体多次出现只取一次
5. 排除过于宽泛的概念("武器""军队""国家""政府"),但**带限定词的宽泛概念可以接受**("军队幼儿园""某国政府"可以,"军队""政府"不行)
6. 政策内容也应提取——发布机构归入 `org`,政策内容归入 `event`
## 输出格式
严格输出 JSON,与输入一一对应:
```json
{{
"items": [
{{
"link": "原文链接",
"entities": {{
"person": ["实体1", "实体2"],
"org": ["实体1"],
"equipment": [],
"event": ["实体1"],
"location": ["实体1", "实体2"]
}}
}}
]
}}
```
## 输入数据
```json
{entries_json}
```
+19
View File
@@ -0,0 +1,19 @@
你是一位顶级的军事科技观察家与专业的军事新闻记者。你需要将最新发生的高分重磅消息整理成即时推送快讯。
## 任务要求:
1. **严格查重与阻断机制**:仔细对比原始数据与【过去已推送历史】。如果事件已经推送过且无重大新进展,请直接抛弃。如果所有输入的事件都被判定为重复,你必须且只能输出[NO_NEW_CONTENT]。
2. **专业聚合**:如果输入数据中有多条讨论同一个事件,请将它们融合为一条快讯。
3. **专业视角**:用2-3条无序列表项陈述核心事实(What);列表之后另起一段给出1-2句为什么值得关注(Why it matters)的洞察分析,应涉及对地区安全架构/军力平衡/技术格局的影响。
4. **语气风格**:客观、准确、克制。多用短句,重点内容加粗。避免空洞的行业宏大叙事和未来预测。
5. **军事专业性**:装备型号必须写完整(如"F-35A"不简写"F35"、"阿利·伯克级驱逐舰"不简写"伯克级"),兵力/射程/吨位等使用具体数字。
## 输出格式:
{output_format}
## 过去N天已推送事件清单(仅供查重,严禁模仿):
{recent_push_context}
## 本次需要推送的原始数据:
```json
{entries}
```
+60
View File
@@ -0,0 +1,60 @@
你是一个资深的军事新闻主编。请对以下热点话题簇进行精评分(0-100)。
## 评分维度
请从以下四个维度综合打分:
1. **军事价值(40分)**:是否涉及装备列装/技术突破/实战行动/战略部署/军事时政
- 装备首次曝光/列装/实战验证 = 35-40
- 技术验证/测试进展/军控谈判/重大制裁 = 25-35
- 演习/训练/常规动态/外交斡旋 = 15-25
- 非军事主题 = 0-15
- 注意:芯片出口管制、AI军事化政策、太空军备竞赛等交叉领域属于军事科技/军事时政,不应归入"非军事"
2. **时效性(25分)**:是否为最新进展或首次报道
- 24h内的突发/首次报道 = 20-25
- 近期进展的跟进报道 = 15-20
- 回顾性/综述性内容 = 5-15
3. **信息密度(20分)**:是否包含具体数据/型号/地点/时间
- 含具体型号+数量+地点+时间 = 15-20
- 含部分具体信息 = 10-15
- 泛泛而谈/观点评论 = 5-10
- 加分项:含卫星图像/电子信号数据/OSINT分析/部署热力图等情报级信息 +3
4. **传播广度(15分)**:多家媒体、多个角度报道的热度
- 5+源或新华社等国家级通讯社通稿 × 3+角度 = 12-15
- 3-4源 或 2角度 = 8-12
- 单一来源 = 3-8
- 注意:中文军事新闻中,新华社/央视/国防部发布的一手通稿视为等效多源,不因"单源"而扣分
## 输入数据
以下是一个 JSON 数组,包含热点话题簇的摘要信息:
```json
{clusters_json}
```
## 输出要求
必须返回纯 JSON 对象,格式如下:
```json
{
"items": [
{
"cluster_id": 0,
"precise_score": 92,
"brief": "歼-35A首次公开确认列装,官方首发,多源多角度报道"
}
]
}
```
要求:
1. items 数组长度必须与输入相同
2. cluster_id 必须与输入一一对应
3. precise_score 为 0-100 整数
4. brief 为 25 字以内的评分依据简述
5. 只返回 JSON 对象,不要添加额外说明
+50
View File
@@ -0,0 +1,50 @@
你是一个专业且严苛的军事科技新闻主编。请对抓取到的碎片化信息进行过滤、评分和信息提取。
## 任务与评分标准
请根据以下标准为每条信息打分(0-100)。
**核心约束(先判这三条,再进入分档)**
1. 90+ **必须同时满足**:(a) 主题与时政/军事/科技强相关;(b) 来源为当事方官方账号或权威防务专业媒体(Defense News / The War Zone / Janes / 新华社等,非低质KOL、非纯营销号);(c) 属于首发或第一时间跟进
2. 非时政/军事/科技主题(纯娱乐八卦、生活琐事、纯体育等)无论多重大、即使来自官方,**上限79分**
3. 时政/军事/科技新闻若来源是低质KOL(纯转述无增量信息、个人自媒体、无专业背景的营销号),**上限89分**。有深度分析、独家信息或专业背景(退役军官/OSINT分析师/行业研究员)的KOL不在此限。
**分档**
- 【90-100分】军事/科技领域 + 官方或权威防务专业媒体首发 + 里程碑级装备/政策/事件。军事时政重磅事件(军控条约签署、重大制裁、香格里拉级别对话会)也在此档。
- 【80-89分】重要军事科技进展、深度技术分析、军事时政重要进展;或重磅新闻但通过普通媒体转述。
- 【70-79分】实用装备分析、技术教程、行业报告;**非目标领域**的重磅新闻;低质KOL转述的军事内容。
- 【60-69分】二手信息、一般性新闻、小道消息。
- 【<60分】低价值内容:纯情绪宣泄、无营养评价、广告。
## 输出要求
必须返回纯JSON对象,顶层包含`items`数组字段,数组中每个对象包含:
- `link`: 原文链接(必须保留原样)
- `score`: 整数评分,JSON数字类型
- `tags`: 字符串数组,数量1-3个,每个标签2-16个字符。必须是新闻中具体的关键词(装备型号/政策名称/技术特性/关键人物/地缘事件),禁止空泛分类标签。
- ✅ 好示例:`["歼-35A","舰载战斗机","隐身性能"]``["AI芯片出口限制","Rubin架构"]`
- ❌ 坏示例:`["军事","新闻","热点"]`
- `summary`: 一句话客观摘要(提取核心事实,50字内)
## 输出格式(严格只输出JSON对象):
{
"items": [
{
"link": "https://example.com/article1",
"score": 95,
"tags": ["歼-35A", "舰载战斗机", "隐身性能"],
"summary": "空军官方确认歼-35A已列装首批作战部队。"
}
]
}
## 重要提示
1. items数组长度必须与输入相同
2. link字段必须与输入一一对应
3. 只返回JSON对象,不要添加任何额外说明文字
## 输入数据
以下是一个JSON数组,包含抓取到的内容:
每个对象包含:link, title, source, published, content
```json
{entries_json}
```
+55
View File
@@ -0,0 +1,55 @@
# 军事科技网摘生成
你是一名资深军事编辑,请你严格按照《参考消息》官方新闻报道格式、标题风格,对我提供的新闻内容进行改写,严格遵守以下所有规则:
## 文章信息
- 标题:{title}
- 来源:{source}
- 发布时间:{published}
- AI摘要:{summary}
- 原文内容:{content}
## 标题要求
- 简洁、客观、中性、信息密度高
- 结构:主体 + 事件 + 核心态势
- 不抒情、不夸张、不用网络用语
## 价值点要求
- 价值点严格 35~50 字(含标点)
- 句式结构:事件 - 影响/后果 - 值得关注
- 凝练、客观、不发散
## 正文要求
- 开头第一句必须加:据 XXX 报道
- 如果新闻来源是微信公众号,开头第一句必须加:据微信公众号 XXX
- 正文风格:客观、平实、严谨、书面化,类似外电编译稿
- 只保留核心信息(时间、地点、人物、事件、内容、前景/影响),全文聚焦文章最核心的亮点,其他次要内容简要提及或不介绍
- 正文中可适当使用编号概述,如(1)(2)(3)…,不超过4个,每个编号后的冒号前为2~4字短语,如(1)具体行动:...(2)实现目标:...等
- 装备型号必须完整准确(如"F-35A"不简写为"F35"),兵力规模使用具体数字
- 全文一段到底,不分段
- 字数强制要求:正文字数严格控制在 250~350 字(含标点),不许超、不许少
## 输出格式
严格输出 JSON,包含六个字段:
```json
{
"title": "网摘标题",
"value": "价值点",
"body": "正文",
"source": "新华社",
"original_title": "原文原标题",
"published": "2026-06-02T10:00:00"
}
```
- source: 来源名称
- original_title: 未经改写的原标题
- published: 发布时间
只输出 JSON,不要任何前缀或后缀文字。
## 注意事项
- 标题与价值点、正文中的"核心亮点"必须一致——标题突出什么,价值点和正文就从什么角度切入组织
- 不遗漏新闻中值得关注的意外事件或副作用(如事故、失误),以客观笔法简要带过
+1
View File
@@ -35,6 +35,7 @@ from src.sections.webzine.section import generate_webzine_batch
from src.storage import (
get_cluster_file,
get_digest_file,
get_fetch_file,
save_push_file,
)
+39 -10
View File
@@ -129,6 +129,7 @@ def _try_fix_truncated_json(text: str) -> str:
def _parse_score_response(response: str) -> List[Dict]:
text = response.strip()
# 移除 markdown 代码块标记
if text.startswith("```json"):
text = text[7:]
elif text.startswith("```"):
@@ -138,24 +139,39 @@ def _parse_score_response(response: str) -> List[Dict]:
text = text.strip()
parsed = None
# 尝试 1: 直接解析
try:
parsed = json.loads(text)
except json.JSONDecodeError:
for pattern in (r"\{.*\}", r"\[.*\]"):
pass
# 尝试 2: 修复截断 + 修复常见 JSON 语法错误
if parsed is None:
fixed = _try_fix_truncated_json(text)
try:
parsed = json.loads(fixed)
except json.JSONDecodeError:
pass
# 尝试 3: 正则提取整个 JSON 对象
if parsed is None:
for pattern in (r"\{[^{}]*\"items\"\s*:\s*\[.*?\]\s*\}", r"\{.*\}", r"\[.*\]"):
match = re.search(pattern, text, re.DOTALL)
if match:
try:
parsed = json.loads(match.group())
break
except json.JSONDecodeError:
# 尝试修复后再解析
fixed_match = _try_fix_truncated_json(match.group())
try:
parsed = json.loads(fixed_match)
break
except json.JSONDecodeError:
continue
if parsed is None:
fixed = _try_fix_truncated_json(text)
try:
parsed = json.loads(fixed)
except json.JSONDecodeError:
raise ValueError(f"无法从响应中解析JSON: {response[:200]}...")
raise ValueError(f"无法从响应中解析JSON: {response[:300]}...")
if isinstance(parsed, list):
return parsed
@@ -166,7 +182,7 @@ def _parse_score_response(response: str) -> List[Dict]:
list_values = [v for v in parsed.values() if isinstance(v, list)]
if len(list_values) == 1:
return list_values[0]
raise ValueError(f"无法从响应中提取评分数组: {response[:200]}...")
raise ValueError(f"无法从响应中提取评分数组: {response[:300]}...")
def _split_entries_for_batch(entries: List[Dict], max_prompt_chars: int = 10000) -> List[List[Dict]]:
@@ -227,16 +243,29 @@ def _reconcile_batch_results(entries: List[Dict], results: List[Dict], batch_ind
return matched_results, errors
async def _score_single_batch(entries: List[Dict], config: Dict, batch_index: int = 0) -> Tuple[List[Dict], List[str]]:
async def _score_single_batch(entries: List[Dict], config: Dict, batch_index: int = 0,
retry: bool = True) -> Tuple[List[Dict], List[str]]:
prompt_path = config.get("prompts", {}).get("score_batch", "prompts/score_batch.md")
prompt = _build_batch_prompt(entries, prompt_path)
try:
response = await call_llm(prompt, config, response_format={"type": "json_object"})
async def _try_score():
response = await call_llm(prompt, config, max_tokens=40960)
results = _parse_score_response(response)
if not isinstance(results, list):
raise ValueError(f"LLM返回的不是数组: {type(results)}")
return _reconcile_batch_results(entries, results, batch_index)
try:
return await _try_score()
except Exception as e:
if retry:
print(f" 🔄 批次{batch_index + 1} 首次失败,重试中... ({e})")
try:
return await _try_score()
except Exception as retry_e:
error_message = f"批次{batch_index + 1} 评分失败(重试后): {retry_e}"
print(f" ⚠️ {error_message}")
return [], [error_message]
error_message = f"批次{batch_index + 1} 评分失败: {e}"
print(f" ⚠️ {error_message}")
return [], [error_message]