239 lines
8.3 KiB
Python
239 lines
8.3 KiB
Python
|
|
# core/requirement_analyzer.py - 需求分解 & 函数签名生成
|
|||
|
|
import re
|
|||
|
|
from typing import List, Optional
|
|||
|
|
|
|||
|
|
import config
|
|||
|
|
from core.llm_client import LLMClient
|
|||
|
|
from database.models import FunctionalRequirement
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RequirementAnalyzer:
|
|||
|
|
"""
|
|||
|
|
使用 LLM 将原始需求分解为功能需求列表,并生成函数接口签名。
|
|||
|
|
支持注入知识库上下文以提升分解质量。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def __init__(self, llm_client: Optional[LLMClient] = None):
|
|||
|
|
"""
|
|||
|
|
初始化需求分析器
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
llm_client: LLM 客户端实例,为 None 时自动创建
|
|||
|
|
"""
|
|||
|
|
self.llm = llm_client or LLMClient()
|
|||
|
|
|
|||
|
|
# ══════════════════════════════════════════════════
|
|||
|
|
# 需求分解
|
|||
|
|
# ══════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
def decompose(
|
|||
|
|
self,
|
|||
|
|
raw_requirement: str,
|
|||
|
|
project_id: int,
|
|||
|
|
raw_req_id: int,
|
|||
|
|
knowledge: str = "",
|
|||
|
|
) -> List[FunctionalRequirement]:
|
|||
|
|
"""
|
|||
|
|
将原始需求分解为功能需求列表
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
raw_requirement: 原始需求文本
|
|||
|
|
project_id: 所属项目 ID
|
|||
|
|
raw_req_id: 原始需求记录 ID
|
|||
|
|
knowledge: 知识库文本(可选)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
FunctionalRequirement 对象列表(未持久化,id=None)
|
|||
|
|
|
|||
|
|
Raises:
|
|||
|
|
ValueError: LLM 返回格式不合法
|
|||
|
|
json.JSONDecodeError: JSON 解析失败
|
|||
|
|
"""
|
|||
|
|
knowledge_section = self._build_knowledge_section(knowledge)
|
|||
|
|
prompt = config.DECOMPOSE_PROMPT_TEMPLATE.format(
|
|||
|
|
knowledge_section=knowledge_section,
|
|||
|
|
raw_requirement=raw_requirement,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
result = self.llm.chat_json(
|
|||
|
|
system_prompt="你是一位资深软件架构师,擅长需求分析与系统设计。",
|
|||
|
|
user_prompt=prompt,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
items = result.get("functional_requirements", [])
|
|||
|
|
if not items:
|
|||
|
|
raise ValueError("LLM 未返回任何功能需求,请检查原始需求描述")
|
|||
|
|
|
|||
|
|
requirements = []
|
|||
|
|
for item in items:
|
|||
|
|
req = FunctionalRequirement(
|
|||
|
|
project_id=project_id,
|
|||
|
|
raw_req_id=raw_req_id,
|
|||
|
|
index_no=int(item.get("index", len(requirements) + 1)),
|
|||
|
|
title=item.get("title", "未命名功能"),
|
|||
|
|
description=item.get("description", ""),
|
|||
|
|
function_name=self._sanitize_function_name(
|
|||
|
|
item.get("function_name", f"func_{len(requirements)+1}")
|
|||
|
|
),
|
|||
|
|
priority=item.get("priority", "medium"),
|
|||
|
|
)
|
|||
|
|
requirements.append(req)
|
|||
|
|
|
|||
|
|
return requirements
|
|||
|
|
|
|||
|
|
# ══════════════════════════════════════════════════
|
|||
|
|
# 函数签名生成(新增)
|
|||
|
|
# ══════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
def build_function_signature(
|
|||
|
|
self,
|
|||
|
|
func_req: FunctionalRequirement,
|
|||
|
|
knowledge: str = "",
|
|||
|
|
) -> dict:
|
|||
|
|
"""
|
|||
|
|
为单个功能需求生成函数接口签名 JSON
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
func_req: 功能需求对象(需含有效 id)
|
|||
|
|
knowledge: 知识库文本(可选)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
符合接口规范的 dict,包含 name/requirement_id/description/
|
|||
|
|
type/parameters/return 字段
|
|||
|
|
|
|||
|
|
Raises:
|
|||
|
|
json.JSONDecodeError: LLM 返回非合法 JSON
|
|||
|
|
"""
|
|||
|
|
requirement_id = self._format_requirement_id(func_req.index_no)
|
|||
|
|
knowledge_section = self._build_knowledge_section(knowledge)
|
|||
|
|
|
|||
|
|
prompt = config.FUNC_SIGNATURE_PROMPT_TEMPLATE.format(
|
|||
|
|
knowledge_section=knowledge_section,
|
|||
|
|
requirement_id=requirement_id,
|
|||
|
|
title=func_req.title,
|
|||
|
|
function_name=func_req.function_name,
|
|||
|
|
description=func_req.description,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
signature = self.llm.chat_json(
|
|||
|
|
system_prompt=(
|
|||
|
|
"你是一位资深软件架构师,专注于 API 接口设计。"
|
|||
|
|
"只输出合法 JSON,不添加任何说明文字。"
|
|||
|
|
),
|
|||
|
|
user_prompt=prompt,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 确保关键字段存在,做兜底处理
|
|||
|
|
signature.setdefault("name", func_req.function_name)
|
|||
|
|
signature.setdefault("requirement_id", requirement_id)
|
|||
|
|
signature.setdefault("description", func_req.description)
|
|||
|
|
signature.setdefault("type", "function")
|
|||
|
|
signature.setdefault("parameters", {})
|
|||
|
|
signature.setdefault("return", {"type": "void", "description": ""})
|
|||
|
|
|
|||
|
|
return signature
|
|||
|
|
|
|||
|
|
def build_function_signatures_batch(
|
|||
|
|
self,
|
|||
|
|
func_reqs: List[FunctionalRequirement],
|
|||
|
|
knowledge: str = "",
|
|||
|
|
on_progress=None,
|
|||
|
|
) -> List[dict]:
|
|||
|
|
"""
|
|||
|
|
批量为功能需求列表生成函数接口签名
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
func_reqs: 功能需求列表
|
|||
|
|
knowledge: 知识库文本(可选)
|
|||
|
|
on_progress: 进度回调 fn(index, total, func_req, signature, error)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
签名 dict 列表,顺序与 func_reqs 一致;
|
|||
|
|
生成失败的条目使用降级结构填充,不中断整体流程
|
|||
|
|
"""
|
|||
|
|
results = []
|
|||
|
|
total = len(func_reqs)
|
|||
|
|
|
|||
|
|
for i, req in enumerate(func_reqs):
|
|||
|
|
try:
|
|||
|
|
sig = self.build_function_signature(req, knowledge)
|
|||
|
|
results.append(sig)
|
|||
|
|
if on_progress:
|
|||
|
|
on_progress(i + 1, total, req, sig, None)
|
|||
|
|
except Exception as e:
|
|||
|
|
# 降级:用基础信息填充,保证 JSON 完整性
|
|||
|
|
fallback = self._build_fallback_signature(req)
|
|||
|
|
results.append(fallback)
|
|||
|
|
if on_progress:
|
|||
|
|
on_progress(i + 1, total, req, fallback, e)
|
|||
|
|
|
|||
|
|
return results
|
|||
|
|
|
|||
|
|
# ══════════════════════════════════════════════════
|
|||
|
|
# 私有工具方法
|
|||
|
|
# ══════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _build_knowledge_section(knowledge: str) -> str:
|
|||
|
|
"""构建知识库 Prompt 段落"""
|
|||
|
|
if not knowledge or not knowledge.strip():
|
|||
|
|
return ""
|
|||
|
|
return f"""## 参考知识库
|
|||
|
|
{knowledge}
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _sanitize_function_name(name: str) -> str:
|
|||
|
|
"""
|
|||
|
|
清理函数名,确保符合 snake_case 规范
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
name: 原始函数名
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
合法的 snake_case 函数名
|
|||
|
|
"""
|
|||
|
|
name = re.sub(r"[^a-zA-Z0-9_]", "_", name).lower()
|
|||
|
|
name = re.sub(r"_+", "_", name).strip("_")
|
|||
|
|
if name and name[0].isdigit():
|
|||
|
|
name = "func_" + name
|
|||
|
|
return name or "unnamed_function"
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _format_requirement_id(index_no: int) -> str:
|
|||
|
|
"""
|
|||
|
|
将序号格式化为需求编号字符串
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
index_no: 功能需求序号(从 1 开始)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
格式化编号,如 'REQ.01'、'REQ.12'
|
|||
|
|
"""
|
|||
|
|
return f"REQ.{index_no:02d}"
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _build_fallback_signature(func_req: FunctionalRequirement) -> dict:
|
|||
|
|
"""
|
|||
|
|
构建降级签名(LLM 调用失败时使用)
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
func_req: 功能需求对象
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
包含基础信息的签名 dict
|
|||
|
|
"""
|
|||
|
|
return {
|
|||
|
|
"name": func_req.function_name,
|
|||
|
|
"requirement_id": f"REQ.{func_req.index_no:02d}",
|
|||
|
|
"description": func_req.description,
|
|||
|
|
"type": "function",
|
|||
|
|
"parameters": {},
|
|||
|
|
"return": {
|
|||
|
|
"type": "void",
|
|||
|
|
"description": "TODO: define return value"
|
|||
|
|
},
|
|||
|
|
"_note": "Auto-generated fallback due to LLM error"
|
|||
|
|
}
|