From 6364fb437cf78381ca0fff670da4faf3903e4e02 Mon Sep 17 00:00:00 2001 From: wuyang6 Date: Thu, 28 May 2026 14:30:55 +0800 Subject: [PATCH] Normalize product data prompt formatting --- .../scripts/batch_label_model.py | 23 +++++++++- skills/product-data/SKILL.md | 1 + .../knowledge/canonical_record_v1.md | 7 ++++ .../scripts/product_data_portable.py | 23 +++++++--- src/data_agent_records.py | 23 +++++++--- tests/test_data_agent_records.py | 42 +++++++++++++++++++ 6 files changed, 106 insertions(+), 13 deletions(-) diff --git a/skills/model-labeling/scripts/batch_label_model.py b/skills/model-labeling/scripts/batch_label_model.py index f177fab..e1011f0 100755 --- a/skills/model-labeling/scripts/batch_label_model.py +++ b/skills/model-labeling/scripts/batch_label_model.py @@ -397,6 +397,8 @@ def build_planning_prompt( def wrap_chat_prompt(system_prompt: str, instruction: str) -> str: + system_prompt = prompt_section_without_trailing_newline(system_prompt) + instruction = prompt_section_with_one_trailing_newline(instruction) return ( f"<|im_start|>system\n{system_prompt}<|im_end|>\n" f"<|im_start|>user\n{instruction}<|im_end|>\n" @@ -404,6 +406,14 @@ def wrap_chat_prompt(system_prompt: str, instruction: str) -> str: ) +def prompt_section_without_trailing_newline(text: str) -> str: + return str(text or "").rstrip("\n") + + +def prompt_section_with_one_trailing_newline(text: str) -> str: + return prompt_section_without_trailing_newline(text) + "\n" + + def build_training_instruction(record: dict[str, Any], session_num: int, session_time_minutes: int) -> str: turn = record.get("turn") if isinstance(record.get("turn"), dict) else {} query = str(turn.get("query") or "") @@ -414,7 +424,7 @@ def build_training_instruction(record: dict[str, Any], session_num: int, session return ( "请参考用户的[当前query]、[对话历史]、[知识注入]、[系统状态]识别出[当前query]的[function]结果,[function]是python的code形式。\n" "[知识注入]\n" - f"{json.dumps({'location': str(context.get('location') or ''), 'rag': str(context.get('rag') or '')}, ensure_ascii=False, indent=0)}\n" + f"{context_prompt_block(context)}\n" "[系统状态]\n" "{}\n" "[对话历史]\n" @@ -425,6 +435,17 @@ def build_training_instruction(record: dict[str, Any], session_num: int, session ) +def context_prompt_block(context: dict[str, Any]) -> str: + fields = ("location", "rag") + lines = ["{"] + for index, field in enumerate(fields): + comma = "," if index < len(fields) - 1 else "" + value = str(context.get(field) or "") + lines.append(f'"{field}": {json.dumps(value, ensure_ascii=False)}{comma}') + lines.append("}") + return "\n".join(lines) + + def render_history(prev_session: list[Any], current_ts: int | None, session_num: int, session_time_minutes: int) -> str: usable: list[dict[str, Any]] = [] for item in prev_session[-session_num:]: diff --git a/skills/product-data/SKILL.md b/skills/product-data/SKILL.md index 35158fa..e0bdd56 100644 --- a/skills/product-data/SKILL.md +++ b/skills/product-data/SKILL.md @@ -242,6 +242,7 @@ canonical records 落盘必须使用 `export_dataset_records.py`,默认格式 - 评测数据:执行 `export_planning_eval_csv.py`,输出 `eval_planning.csv`,字段为 `request_id,newPrompt,query,类别真实标签,code标签,complex`。 - 训练 `output` 和流转表格 `function` 列都会自动组合为两行:第一行 `complex=true/false`,第二行原监督标签;评测 `complex` 列直接来自 canonical record 的 `dimensions.complex`,按评测表习惯输出 `TRUE/FALSE`。 - 训练 `instruction` 和评测 `newPrompt` 复用相同 prompt 主体:`[知识注入]`、`[系统状态]`、`[对话历史]`、`[当前query]`、`[function]`;评测 `newPrompt` 还会额外包上 `<|im_start|>system`、`<|im_start|>user`、`<|im_start|>assistant` chat template。 +- prompt 格式必须由转换脚本生成并保持稳定:`[知识注入]` 固定为多行 JSON 块,当前 query 固定写成 `用户: `,`[function]` 后必须保留换行;不要在 `[function]` 下追加 `def Agent(...)` / `def Skill(...)` 等签名说明。 - 对话历史默认最多取 5 轮,且相邻轮间隔不超过 5 分钟;`context` 默认注入 `location` 和 `rag` 两个字段。 当前 canonical record v1 工作格式: diff --git a/skills/product-data/knowledge/canonical_record_v1.md b/skills/product-data/knowledge/canonical_record_v1.md index 7e50098..d642490 100644 --- a/skills/product-data/knowledge/canonical_record_v1.md +++ b/skills/product-data/knowledge/canonical_record_v1.md @@ -68,3 +68,10 @@ canonical record 是产品数据生成链路的中间元数据格式。它不是 - `eval_planning.csv`:字段为 `request_id,newPrompt,query,类别真实标签,code标签,complex`,由 `export_planning_eval_csv.py` 生成;`newPrompt` 会包上 `<|im_start|>system/user/assistant` chat template;`complex` 列来自 `dimensions.complex`,按评测表习惯输出 `TRUE/FALSE`,不是固定默认值。 `instruction` 和 `newPrompt` 使用同一套 prompt 主体,默认包含 `[知识注入]`、`[系统状态]`、`[对话历史]`、`[当前query]`、`[function]`。历史轮次默认最多取 5 轮,且相邻时间间隔不超过 5 分钟。 + +格式约束: + +- `[知识注入]` 必须是多行 JSON 块,默认只输出 `location` 和 `rag` 两个字段。 +- `[当前query]` 下一行必须带 `用户:` 前缀。 +- `[function]` 后必须保留换行;评测 `newPrompt` 中随后才是 `<|im_end|>`。 +- 不要在 `[function]` 下追加 `def Agent(...)`、`def Skill(...)` 等函数签名说明。 diff --git a/skills/product-data/scripts/product_data_portable.py b/skills/product-data/scripts/product_data_portable.py index 492bb92..ea7346c 100644 --- a/skills/product-data/scripts/product_data_portable.py +++ b/skills/product-data/scripts/product_data_portable.py @@ -651,12 +651,15 @@ def build_planning_prompt( ) -> str: """把训练 instruction 包成评测侧使用的 chat template。""" - instruction = build_training_instruction( - record, - session_num=session_num, - session_time_minutes=session_time_minutes, - context_fields=context_fields, + instruction = prompt_section_with_one_trailing_newline( + build_training_instruction( + record, + session_num=session_num, + session_time_minutes=session_time_minutes, + context_fields=context_fields, + ) ) + system_prompt = prompt_section_without_trailing_newline(system_prompt) return ( f"<|im_start|>system\n{system_prompt}<|im_end|>\n" f"<|im_start|>user\n{instruction}<|im_end|>\n" @@ -664,6 +667,14 @@ def build_planning_prompt( ) +def prompt_section_without_trailing_newline(text: str) -> str: + return str(text or "").rstrip("\n") + + +def prompt_section_with_one_trailing_newline(text: str) -> str: + return prompt_section_without_trailing_newline(text) + "\n" + + def training_jsonl_line( record: dict[str, Any], *, @@ -673,7 +684,7 @@ def training_jsonl_line( system_prompt: str, ) -> str: payload = { - "system": system_prompt, + "system": prompt_section_without_trailing_newline(system_prompt), "instruction": build_training_instruction( record, session_num=session_num, diff --git a/src/data_agent_records.py b/src/data_agent_records.py index 550a088..42503b9 100644 --- a/src/data_agent_records.py +++ b/src/data_agent_records.py @@ -732,12 +732,15 @@ def build_planning_prompt( ) -> str: """把训练 instruction 包成评测侧使用的 chat template。""" - instruction = build_training_instruction( - record, - session_num=session_num, - session_time_minutes=session_time_minutes, - context_fields=context_fields, + instruction = _prompt_section_with_one_trailing_newline( + build_training_instruction( + record, + session_num=session_num, + session_time_minutes=session_time_minutes, + context_fields=context_fields, + ) ) + system_prompt = _prompt_section_without_trailing_newline(system_prompt) return ( f'<|im_start|>system\n{system_prompt}<|im_end|>\n' f'<|im_start|>user\n{instruction}<|im_end|>\n' @@ -745,6 +748,14 @@ def build_planning_prompt( ) +def _prompt_section_without_trailing_newline(text: str) -> str: + return str(text or '').rstrip('\n') + + +def _prompt_section_with_one_trailing_newline(text: str) -> str: + return _prompt_section_without_trailing_newline(text) + '\n' + + def _training_jsonl_line( record: dict[str, Any], *, @@ -754,7 +765,7 @@ def _training_jsonl_line( system_prompt: str, ) -> str: payload = { - 'system': system_prompt, + 'system': _prompt_section_without_trailing_newline(system_prompt), 'instruction': build_training_instruction( record, session_num=session_num, diff --git a/tests/test_data_agent_records.py b/tests/test_data_agent_records.py index 20c9df0..eed486d 100644 --- a/tests/test_data_agent_records.py +++ b/tests/test_data_agent_records.py @@ -10,6 +10,7 @@ from pathlib import Path from src.agent_tools import build_tool_context, default_tool_registry, execute_tool from src.agent_types import AgentRuntimeConfig from src.data_agent_records import ( + build_planning_prompt, confirm_generation_goal, confirm_generation_plan, export_dataset_records, @@ -262,6 +263,47 @@ target: Agent(tag='地图导航') self.assertIn('[function]', rows[0]['newPrompt']) self.assertTrue(rows[0]['newPrompt'].endswith('<|im_start|>assistant\n')) + def test_build_planning_prompt_matches_standard_chat_template(self) -> None: + records = normalize_dataset_draft( + ''' +# dataset_label: 地图导航边界数据 +### case: 多轮顺路停车场 +用户: 帮我查一下最近的停车场 +小爱: 最近的停车场离你27米 +用户: 帮我找一个最顺路的停车场 +target: Agent(tag='地图导航') +'''.strip(), + batch_id='demo', + base_timestamp=1_755_567_930_500, + timestamp_step_ms=60_000, + )['records'] + records[0]['context'] = {'location': '', 'rag': ''} + + prompt = build_planning_prompt(records[0], system_prompt='你是小爱同学,中文智能语音助手。\n') + + self.assertEqual( + prompt, + '<|im_start|>system\n' + '你是小爱同学,中文智能语音助手。<|im_end|>\n' + '<|im_start|>user\n' + '请参考用户的[当前query]、[对话历史]、[知识注入]、[系统状态]识别出[当前query]的[function]结果,[function]是python的code形式。\n' + '[知识注入]\n' + '{\n' + '"location": "",\n' + '"rag": ""\n' + '}\n' + '[系统状态]\n' + '{}\n' + '[对话历史]\n' + '用户: 帮我查一下最近的停车场\n' + '小爱: 最近的停车场离你27米\n' + '[当前query]\n' + '用户: 帮我找一个最顺路的停车场\n' + '[function]\n' + '<|im_end|>\n' + '<|im_start|>assistant\n', + ) + def test_export_planning_eval_csv_writes_file(self) -> None: records = normalize_dataset_draft( '''