feat(api): 统一 LLM JSON 调用层 llm_json_call,按域 Schema 迁移 chat/memoir agents

This commit is contained in:
Kevin
2026-04-03 13:34:27 +08:00
parent 41518bda11
commit 43d1689e9c
28 changed files with 1006 additions and 352 deletions

View File

@@ -9,6 +9,7 @@ from typing import Optional
from app.agents.chat.background_voice import get_background_voice_narrative_block
from app.agents.chat.occupation_context import get_occupation_narrative_hint
from app.agents.stage_constants import STAGE_ERA_HINTS, STAGE_SLOT_KEYS
from app.features.memory.evidence_format import (
dedupe_evidence_chunk_rows,
format_evidence_chunks_for_prompt,
@@ -119,9 +120,8 @@ childhood, education, career_early, career_achievement, career_challenge, family
对话内容:
{segments_text}
**JSON 输出**`response_format=json_object`,只输出:
输出形状(仅此对象)
{{"category": "childhood|education|career_early|career_achievement|career_challenge|family|beliefs|summary|none"}}
不要其它文字。
若你返回 **none**,服务端会将本段映射到 **summary** 章节并仍写入回忆录正文(不落库丢弃)。"""
@@ -131,21 +131,13 @@ def get_state_extraction_prompt(
) -> str:
"""抽取结构化信息并判断阶段"""
slot_keys = list(stage_slots.keys())
all_stage_slots = {
"childhood": ["place", "people", "daily_life", "emotion", "turning_event"],
"education": ["school", "city", "motivation", "challenge", "change"],
"career": ["job", "environment", "decision", "pressure", "growth"],
"family": ["relationship", "conflict", "support", "responsibility", "change"],
"belief": ["value", "regret", "pride", "lesson"],
}
all_stage_slots = {k: list(v) for k, v in STAGE_SLOT_KEYS.items()}
return f"""你是回忆录访谈信息抽取助手。从用户话语中提取结构化信息,判断用户实际在谈论哪个人生阶段。
只提取口述中确有依据的片段,不得编造或推测。
你需要从用户话语中**先提炼与人生经历相关的核心内容**然后抽取结构化信息slots 仅填口述中确有依据的片段)。
**JSON 输出**:接口已启用 `response_format=json_object`,你必须只输出一个合法 JSON 对象,不要 markdown 代码块或其它文字。
系统当前跟踪的阶段:{current_stage}
该阶段可填 slots{slot_keys}
@@ -189,6 +181,10 @@ def get_batch_memoir_phase1_prep_prompt(
for sid, text in segment_items:
lines.append(f"- id={sid}\n 文本:{text}")
slot_lines = "\n".join(
f"- {st}: {', '.join(keys)}" for st, keys in STAGE_SLOT_KEYS.items()
)
return f"""你是回忆录访谈助手。下面有多段用户口述(按时间顺序),请**逐段**完成:
1信息抽取slots、detected_stage——规则与单段抽取相同
2章节分类chapter_category——规则与单段分类相同。
@@ -199,11 +195,7 @@ def get_batch_memoir_phase1_prep_prompt(
detected_stage 仅允许childhood | education | career | family | belief
slots 的 key 必须属于该 detected_stage 对应集合:
- childhood: place, people, daily_life, emotion, turning_event
- education: school, city, motivation, challenge, change
- career: job, environment, decision, pressure, growth
- family: relationship, conflict, support, responsibility, change
- belief: value, regret, pride, lesson
{slot_lines}
chapter_category 仅允许childhood | education | career_early | career_achievement | career_challenge | family | beliefs | summary | **none**
(不足以成篇的档案点/纯寒暄 → **none**;与单段分类一致。)
@@ -211,7 +203,7 @@ chapter_category 仅允许childhood | education | career_early | career_achie
逐段任务(按下列列表顺序,**segments 数组须覆盖每一行 id且顺序一致**
{chr(10).join(lines)}
**JSON 输出**:只输出一个合法 JSON 对象,不要 markdown格式:
输出 JSON 对象(无 markdown格式:
{{
"segments": [
{{
@@ -228,22 +220,10 @@ chapter_category 仅允许childhood | education | career_early | career_achie
def _build_age_hint(stage: str, birth_year: Optional[int] = None) -> str:
"""根据人生阶段和出生年份推算大致年龄区间"""
"""根据人生阶段和出生年份推算大致年龄区间`STAGE_ERA_HINTS`,仅作提示)。"""
if not birth_year:
return ""
stage_age_ranges = {
"childhood": (0, 12),
"education": (6, 22),
"career": (18, 60),
"career_early": (18, 30),
"career_achievement": (25, 55),
"career_challenge": (20, 55),
"family": (20, 60),
"belief": (30, 70),
"beliefs": (30, 70),
"summary": (50, 80),
}
age_range = stage_age_ranges.get(stage)
age_range = STAGE_ERA_HINTS.get(stage)
if not age_range:
return ""
year_start = birth_year + age_range[0]
@@ -298,9 +278,8 @@ def get_creative_title_json_prompt(
)
return (
base.rstrip()
+ "\n\n**JSON 输出**`response_format=json_object`,只输出"
+ "\n\n输出示例(仅此 JSON 对象)"
+ '\n{"title":"完整标题一行(含时间标注 · 正文格式)"}\n'
+ "不要其它文字。"
)
@@ -331,8 +310,7 @@ def get_narrative_json_prompt(
return f"""{get_narrative_editor_system_prompt(background_voice=background_voice, occupation=occupation)}
请将「本段用户口述」改写为第一人称书面叙述,并输出 **纯 JSON**,不要包含任何其他文字或 markdown 代码块
**JSON 输出**:接口已启用 `response_format=json_object`(与 DeepSeek JSON 模式一致),只输出一个合法 JSON 对象。
请将「本段用户口述」改写为第一人称书面叙述,并输出 **纯 JSON**(无 markdown 围栏)
阶段:{stage}
可用信息slots{slots}{profile_section}{time_section}
@@ -343,7 +321,7 @@ def get_narrative_json_prompt(
## 要求
1. **格式与输出**:只输出 JSON第一人称不使用 `#`、`##`、表格;`content` 仅含正文。
2. **事实与取材**(须遵守系统说明中的事实边界规则 14。只展开「本段用户口述」;若有参考摘录区,不得把摘录中的具体事实写成本轮亲历;过滤语气词与寒暄;不重复已有故事全文;本批同一主题/事件链;段落数量与长度随材料,禁止为凑字数编造。
2. **事实与取材**遵守事实边界,不补写未给出的细节。只展开「本段用户口述」;若有参考摘录区,不得把摘录中的具体事实写成本轮亲历;过滤语气词与寒暄;不重复已有故事全文;本批同一主题/事件链;段落数量与长度随材料,禁止为凑字数编造。
3. **不推断结局**:用户未明确说结果(是否录取、是否被选中等)时,不要凭常识补全为确定结论。
## 输出格式(严格 JSON
@@ -409,8 +387,6 @@ def get_narrative_merge_json_prompt(
你正在**扩写并重组**一则已有回忆录故事:必须把「已有故事」中的事实全部保留在输出中(可合并重复表述、调整语序),并融入「本段用户口述」中的新事实;按**事件发生的时间顺序**排列段落(早→晚);禁止丢弃未矛盾的旧内容。
**JSON 输出**:接口已启用 `response_format=json_object`,只输出一个合法 JSON 对象,不要 markdown 代码块。
阶段:{stage}
可用信息slots{slots}{profile_section}{time_section}
@@ -420,7 +396,7 @@ def get_narrative_merge_json_prompt(
## 要求
1. **全文输出**`paragraphs` 须为重组后的**完整故事正文**(非仅本段)。
2. **事实边界**(须遵守系统说明中的事实边界规则 14。不得新增「已有」或「本段」未出现的人名、地点、时间、对话、数字;第一人称、优雅书面语须符合上文传记作家文体说明;不用 `#`、`##`、表格。
2. **事实边界**遵守事实边界,不补写未给出的细节。不得新增「已有」或「本段」未出现的人名、地点、时间、对话、数字;第一人称、优雅书面语须符合上文传记作家文体说明;不用 `#`、`##`、表格。
3. 若本段与旧文完全重复或无新信息,可输出与旧文等价重组的正文(不得无故缩短到明显少于旧文)。
4. **不推断结局**:本段未明确结果时,不要补全落选/未通过等确定说法,除非旧文中已有同一事实。
@@ -485,8 +461,6 @@ def get_story_route_prompt(
merge_hint = story_route_merge_hint_for_category(chapter_category)
return f"""你是回忆录编辑助手。根据本批用户口述与【候选故事】决定 append_story 或 new_story。
**JSON 输出**:接口已启用 `response_format=json_object`,只输出下面 schema 的一个合法 JSON 对象,不要 markdown。
## 两层决策标准(必须先在心里过一遍)
1. **主题连续性信号**:价值观、关系模式、长期总结、同一反思维度;口述是否像在**同一主题容器**里加厚?
2. **事件切换信号**:是否出现**新人物组合、新地点、新时间段、新事件因果链**,与候选正文明显是**另一段经历**
@@ -535,8 +509,6 @@ def get_story_batch_plan_prompt(
merge_hint = story_route_merge_hint_for_category(chapter_category)
return f"""你是回忆录编辑助手。下面同一章节类别下有一批**按时间顺序**的用户口述片段(每段有 id 与文本)。
**JSON 输出**:接口已启用 `response_format=json_object`,只输出下面 schema 的一个合法 JSON 对象,不要 markdown。
## 两层决策标准(每一块都要应用)
1. **主题连续性信号**:价值观、关系模式、长期总结、同一反思维度。
2. **事件切换信号**:新人物组合、新地点、新时间段、新事件因果链。