feat(data-process): 支持思维链输出类型

This commit is contained in:
caoxiaozhu
2026-07-27 13:08:44 +08:00
parent ecafb7eb13
commit de2e8952b5
14 changed files with 443 additions and 46 deletions

View File

@@ -22,6 +22,11 @@ class ModelGenerationError(ValueError):
"""模型配置、响应或调用失败。"""
OUTPUT_TYPE_STANDARD = "standard"
OUTPUT_TYPE_REASONING = "reasoning"
SUPPORTED_OUTPUT_TYPES = {OUTPUT_TYPE_STANDARD, OUTPUT_TYPE_REASONING}
def chat_completions_url(value: str) -> str:
"""把域名、基础 URL 或完整地址统一为 chat completions 地址。"""
@@ -52,7 +57,9 @@ def _message_content(payload: Mapping[str, Any]) -> str:
try:
content = payload["choices"][0]["message"]["content"]
except (KeyError, IndexError, TypeError) as exc:
raise ModelGenerationError("model response does not contain choices[0].message.content") from exc
raise ModelGenerationError(
"model response does not contain choices[0].message.content"
) from exc
if isinstance(content, str):
return content
if isinstance(content, list):
@@ -67,7 +74,14 @@ def _message_content(payload: Mapping[str, Any]) -> str:
def _json_payload(content: str) -> Any:
cleaned = re.sub(r"<think>[\s\S]*?</think>", "", content, flags=re.IGNORECASE).strip()
# 只移除模型在 JSON 之前自行输出的思考过程,不能破坏 JSON 字段中的训练内容。
cleaned = re.sub(
r"^\s*<think>[\s\S]*?</think>\s*",
"",
content,
count=1,
flags=re.IGNORECASE,
).strip()
fenced = re.fullmatch(r"```(?:json)?\s*([\s\S]*?)\s*```", cleaned, flags=re.IGNORECASE)
if fenced:
cleaned = fenced.group(1).strip()
@@ -107,19 +121,27 @@ def _prompt_messages(
*,
start_index: int,
total_count: int,
output_type: str,
) -> list[dict[str, str]]:
end_index = start_index + count - 1
if output_type == OUTPUT_TYPE_REASONING:
schema = '{"items":[{"instruction":"...","input":"...","reasoning":"...","answer":"..."}]}'
output_rule = (
"instruction、reasoning 和 answer 均不得为空reasoning 必须是基于来源内容、"
"可核对且简洁的推理步骤answer 只写最终答案。"
"这是思维链输出模式,即使其他提示语要求省略分析,也不得省略 reasoning。"
"不要自行添加 <think> 标签,系统会在保存时统一组装。"
)
else:
schema = '{"items":[{"instruction":"...","input":"...","output":"..."}]}'
output_rule = "instruction 和 output 不得为空,不要输出分析过程。"
schema_instruction = (
f"必须只返回 JSON 对象,格式为 {{\"items\":[{{\"instruction\":\"...\","
f"\"input\":\"...\",\"output\":\"...\"}}]}}items 必须包含 {count} 条。"
f"必须只返回 JSON 对象,格式为 {schema}items 必须包含 {count} 条。"
f"这是总计 {total_count} 条中的第 {start_index}-{end_index} 条,"
"各条必须使用不同的提问角度和表述,避免重复。"
"instruction 和 output 不得为空,不要输出 Markdown 代码围栏或分析过程"
)
base_prompt = (
normalize_text(prompt)
or "请根据来源内容生成可用于监督微调的问答数据。"
f"{output_rule}不要输出 Markdown 代码围栏或 JSON 之外的说明"
)
base_prompt = normalize_text(prompt) or "请根据来源内容生成可用于监督微调的问答数据。"
if "{{ content }}" in base_prompt:
user_prompt = base_prompt.replace("{{ content }}", content)
return [
@@ -150,9 +172,10 @@ def generate_model_records(
"""
if not 1 <= qa_pairs_per_item <= MAX_QA_PAIRS_PER_ITEM:
raise ModelGenerationError(
f"qa_pairs_per_item must be in [1, {MAX_QA_PAIRS_PER_ITEM}]"
)
raise ModelGenerationError(f"qa_pairs_per_item must be in [1, {MAX_QA_PAIRS_PER_ITEM}]")
output_type = str(config.get("output_type") or OUTPUT_TYPE_STANDARD).strip().lower()
if output_type not in SUPPORTED_OUTPUT_TYPES:
raise ModelGenerationError(f"output_type must be one of {sorted(SUPPORTED_OUTPUT_TYPES)}")
endpoint = chat_completions_url(str(model.get("api_url") or ""))
model_name = str(model.get("online_model_name") or model.get("name") or "").strip()
if not model_name:
@@ -193,6 +216,7 @@ def generate_model_records(
batch_count,
start_index=batch_start,
total_count=qa_pairs_per_item,
output_type=output_type,
),
"temperature": temperature,
"max_tokens": max_tokens,
@@ -212,12 +236,8 @@ def generate_model_records(
response.raise_for_status()
body = response.json()
if not isinstance(body, Mapping):
raise ModelGenerationError(
"model response body must be a JSON object"
)
candidate_items = _result_items(
_json_payload(_message_content(body))
)
raise ModelGenerationError("model response body must be a JSON object")
candidate_items = _result_items(_json_payload(_message_content(body)))
if len(candidate_items) < batch_count:
raise ModelGenerationError(
"model response contains fewer result objects than requested: "
@@ -266,19 +286,56 @@ def generate_model_records(
input_text = normalize_text(
str(value.get("input") or value.get("context") or "")
)
output = normalize_text(
str(
value.get("output")
or value.get("answer")
or value.get("response")
or ""
if output_type == OUTPUT_TYPE_REASONING:
reasoning = normalize_text(
re.sub(
r"</?think>",
"",
str(value.get("reasoning") or value.get("analysis") or ""),
flags=re.IGNORECASE,
)
)
)
answer = normalize_text(
re.sub(
r"</?think>",
"",
str(
value.get("answer")
or value.get("final_answer")
or value.get("output")
or ""
),
flags=re.IGNORECASE,
)
)
output = (
f"<think>\n{reasoning}\n</think>\n{answer}"
if reasoning and answer
else answer or (f"<think>\n{reasoning}\n</think>" if reasoning else "")
)
valid = bool(instruction and reasoning and answer)
missing_error = "model result is missing instruction, reasoning or answer"
else:
output = normalize_text(
str(
value.get("output")
or value.get("answer")
or value.get("response")
or ""
)
)
output = normalize_text(
re.sub(
r"<think>[\s\S]*?(?:</think>|$)",
"",
output,
flags=re.IGNORECASE,
)
)
valid = bool(instruction and output)
missing_error = "model result is missing instruction or output"
raw_id = f"{preview_id}:{variant_index + 1}:{instruction}:{output}"
result_id = (
f"result_{hashlib.sha256(raw_id.encode()).hexdigest()[:16]}"
)
valid = bool(instruction and output)
result_id = f"result_{hashlib.sha256(raw_id.encode()).hexdigest()[:16]}"
results.append(
{
"id": result_id,
@@ -290,11 +347,7 @@ def generate_model_records(
"original_input": input_text,
"original_output": output,
"status": "valid" if valid else "invalid",
"error": (
None
if valid
else "model result is missing instruction or output"
),
"error": (None if valid else missing_error),
"split": "train",
}
)