feat: 实现 Prompt 模板化功能 (Phase 10 T10.1-T10.8) [AC-AISVC-51~AC-AISVC-58]

- 新增 PromptTemplate 和 PromptTemplateVersion SQLModel 实体
- 实现 PromptTemplateService:模板 CRUD、版本管理、发布/回滚、缓存
- 实现 VariableResolver:内置变量注入和自定义变量替换
- 实现 Prompt 模板管理 API(CRUD + 发布/回滚)
- T10.9(修改 Orchestrator)和 T10.10(单元测试)留待集成阶段
This commit is contained in:
MerCry 2026-02-27 14:15:10 +08:00
parent 0f1fa7de5c
commit eb93636227
9 changed files with 1163 additions and 50 deletions

View File

@ -6,10 +6,12 @@ Admin API routes for AI Service management.
from app.api.admin.api_key import router as api_key_router
from app.api.admin.dashboard import router as dashboard_router
from app.api.admin.embedding import router as embedding_router
from app.api.admin.intent_rules import router as intent_rules_router
from app.api.admin.kb import router as kb_router
from app.api.admin.llm import router as llm_router
from app.api.admin.prompt_templates import router as prompt_templates_router
from app.api.admin.rag import router as rag_router
from app.api.admin.sessions import router as sessions_router
from app.api.admin.tenants import router as tenants_router
__all__ = ["api_key_router", "dashboard_router", "embedding_router", "kb_router", "llm_router", "rag_router", "sessions_router", "tenants_router"]
__all__ = ["api_key_router", "dashboard_router", "embedding_router", "intent_rules_router", "kb_router", "llm_router", "prompt_templates_router", "rag_router", "sessions_router", "tenants_router"]

View File

@ -0,0 +1,208 @@
"""
Prompt Template Management API.
[AC-AISVC-52, AC-AISVC-57, AC-AISVC-58, AC-AISVC-54, AC-AISVC-55] Prompt template CRUD and publish/rollback endpoints.
"""
import logging
import uuid
from typing import Any
from fastapi import APIRouter, Depends, Header, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_session
from app.models.entities import PromptTemplateCreate, PromptTemplateUpdate
from app.services.prompt.template_service import PromptTemplateService
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/admin/prompt-templates", tags=["Prompt Management"])
def get_tenant_id(x_tenant_id: str = Header(..., alias="X-Tenant-Id")) -> str:
"""Extract tenant ID from header."""
if not x_tenant_id:
raise HTTPException(status_code=400, detail="X-Tenant-Id header is required")
return x_tenant_id
@router.get("")
async def list_templates(
tenant_id: str = Depends(get_tenant_id),
scene: str | None = None,
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-57] List all prompt templates for a tenant.
"""
logger.info(f"[AC-AISVC-57] Listing prompt templates for tenant={tenant_id}, scene={scene}")
service = PromptTemplateService(session)
templates = await service.list_templates(tenant_id, scene)
data = []
for t in templates:
published_version = await service.get_published_version_info(tenant_id, t.id)
data.append({
"id": str(t.id),
"name": t.name,
"scene": t.scene,
"description": t.description,
"is_default": t.is_default,
"published_version": published_version,
"created_at": t.created_at.isoformat(),
"updated_at": t.updated_at.isoformat(),
})
return {"data": data}
@router.post("", status_code=201)
async def create_template(
body: PromptTemplateCreate,
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-52] Create a new prompt template.
"""
logger.info(f"[AC-AISVC-52] Creating prompt template for tenant={tenant_id}, name={body.name}")
service = PromptTemplateService(session)
template = await service.create_template(tenant_id, body)
return {
"id": str(template.id),
"name": template.name,
"scene": template.scene,
"description": template.description,
"is_default": template.is_default,
"created_at": template.created_at.isoformat(),
"updated_at": template.updated_at.isoformat(),
}
@router.get("/{tpl_id}")
async def get_template_detail(
tpl_id: uuid.UUID,
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-58] Get prompt template detail with version history.
"""
logger.info(f"[AC-AISVC-58] Getting template detail for tenant={tenant_id}, id={tpl_id}")
service = PromptTemplateService(session)
detail = await service.get_template_detail(tenant_id, tpl_id)
if not detail:
raise HTTPException(status_code=404, detail="Template not found")
return detail
@router.put("/{tpl_id}")
async def update_template(
tpl_id: uuid.UUID,
body: PromptTemplateUpdate,
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-53] Update prompt template (creates a new version).
"""
logger.info(f"[AC-AISVC-53] Updating template for tenant={tenant_id}, id={tpl_id}")
service = PromptTemplateService(session)
template = await service.update_template(tenant_id, tpl_id, body)
if not template:
raise HTTPException(status_code=404, detail="Template not found")
published_version = await service.get_published_version_info(tenant_id, template.id)
return {
"id": str(template.id),
"name": template.name,
"scene": template.scene,
"description": template.description,
"is_default": template.is_default,
"published_version": published_version,
"created_at": template.created_at.isoformat(),
"updated_at": template.updated_at.isoformat(),
}
@router.post("/{tpl_id}/publish")
async def publish_template(
tpl_id: uuid.UUID,
body: dict[str, int],
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-54] Publish a specific version of the template.
"""
version = body.get("version")
if version is None:
raise HTTPException(status_code=400, detail="version is required")
logger.info(
f"[AC-AISVC-54] Publishing template version for tenant={tenant_id}, "
f"id={tpl_id}, version={version}"
)
service = PromptTemplateService(session)
success = await service.publish_version(tenant_id, tpl_id, version)
if not success:
raise HTTPException(status_code=404, detail="Template or version not found")
return {"success": True, "message": f"Version {version} published successfully"}
@router.post("/{tpl_id}/rollback")
async def rollback_template(
tpl_id: uuid.UUID,
body: dict[str, int],
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-55] Rollback to a specific historical version.
"""
version = body.get("version")
if version is None:
raise HTTPException(status_code=400, detail="version is required")
logger.info(
f"[AC-AISVC-55] Rolling back template for tenant={tenant_id}, "
f"id={tpl_id}, version={version}"
)
service = PromptTemplateService(session)
success = await service.rollback_version(tenant_id, tpl_id, version)
if not success:
raise HTTPException(status_code=404, detail="Template or version not found")
return {"success": True, "message": f"Rolled back to version {version} successfully"}
@router.delete("/{tpl_id}", status_code=204)
async def delete_template(
tpl_id: uuid.UUID,
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> None:
"""
Delete a prompt template and all its versions.
"""
logger.info(f"Deleting template for tenant={tenant_id}, id={tpl_id}")
service = PromptTemplateService(session)
success = await service.delete_template(tenant_id, tpl_id)
if not success:
raise HTTPException(status_code=404, detail="Template not found")

View File

@ -12,7 +12,7 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from app.api import chat_router, health_router
from app.api.admin import api_key_router, dashboard_router, embedding_router, kb_router, llm_router, rag_router, sessions_router, tenants_router
from app.api.admin import api_key_router, dashboard_router, embedding_router, intent_rules_router, kb_router, llm_router, prompt_templates_router, rag_router, sessions_router, tenants_router
from app.api.admin.kb_optimized import router as kb_optimized_router
from app.core.config import get_settings
from app.core.database import close_db, init_db
@ -130,9 +130,11 @@ app.include_router(chat_router)
app.include_router(api_key_router)
app.include_router(dashboard_router)
app.include_router(embedding_router)
app.include_router(intent_rules_router)
app.include_router(kb_router)
app.include_router(kb_optimized_router)
app.include_router(llm_router)
app.include_router(prompt_templates_router)
app.include_router(rag_router)
app.include_router(sessions_router)
app.include_router(tenants_router)

View File

@ -118,20 +118,34 @@ class Tenant(SQLModel, table=True):
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
class KBType(str, Enum):
PRODUCT = "product"
FAQ = "faq"
SCRIPT = "script"
POLICY = "policy"
GENERAL = "general"
class KnowledgeBase(SQLModel, table=True):
"""
[AC-ASA-01] Knowledge base entity with tenant isolation.
[AC-ASA-01, AC-AISVC-59] Knowledge base entity with tenant isolation.
[v0.6.0] Extended with kb_type, priority, is_enabled, doc_count for multi-KB management.
"""
__tablename__ = "knowledge_bases"
__table_args__ = (
Index("ix_knowledge_bases_tenant_id", "tenant_id"),
Index("ix_knowledge_bases_tenant_kb_type", "tenant_id", "kb_type"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
name: str = Field(..., description="Knowledge base name")
kb_type: str = Field(default=KBType.GENERAL.value, description="Knowledge base type: product/faq/script/policy/general")
description: str | None = Field(default=None, description="Knowledge base description")
priority: int = Field(default=0, ge=0, description="Priority weight, higher value means higher priority")
is_enabled: bool = Field(default=True, description="Whether the knowledge base is enabled")
doc_count: int = Field(default=0, ge=0, description="Document count (cached statistic)")
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
@ -184,9 +198,20 @@ class IndexJob(SQLModel, table=True):
class KnowledgeBaseCreate(SQLModel):
"""Schema for creating a new knowledge base."""
tenant_id: str
name: str
kb_type: str = KBType.GENERAL.value
description: str | None = None
priority: int = 0
class KnowledgeBaseUpdate(SQLModel):
"""Schema for updating a knowledge base."""
name: str | None = None
kb_type: str | None = None
description: str | None = None
priority: int | None = None
is_enabled: bool | None = None
class DocumentCreate(SQLModel):
@ -222,3 +247,181 @@ class ApiKeyCreate(SQLModel):
key: str
name: str
is_active: bool = True
class TemplateVersionStatus(str, Enum):
DRAFT = "draft"
PUBLISHED = "published"
ARCHIVED = "archived"
class PromptTemplate(SQLModel, table=True):
"""
[AC-AISVC-51, AC-AISVC-52] Prompt template entity with tenant isolation.
Main table for storing template metadata.
"""
__tablename__ = "prompt_templates"
__table_args__ = (
Index("ix_prompt_templates_tenant_scene", "tenant_id", "scene"),
Index("ix_prompt_templates_tenant_id", "tenant_id"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
name: str = Field(..., description="Template name, e.g., 'Default Customer Service Persona'")
scene: str = Field(..., description="Scene tag: chat/rag_qa/greeting/farewell")
description: str | None = Field(default=None, description="Template description")
is_default: bool = Field(default=False, description="Whether this is the default template for the scene")
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
class PromptTemplateVersion(SQLModel, table=True):
"""
[AC-AISVC-53] Prompt template version entity.
Stores versioned content with status management.
"""
__tablename__ = "prompt_template_versions"
__table_args__ = (
Index("ix_template_versions_template_status", "template_id", "status"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
template_id: uuid.UUID = Field(..., description="Foreign key to prompt_templates.id", foreign_key="prompt_templates.id", index=True)
version: int = Field(..., description="Version number (auto-incremented per template)")
status: str = Field(default=TemplateVersionStatus.DRAFT.value, description="Version status: draft/published/archived")
system_instruction: str = Field(..., description="System instruction content with {{variable}} placeholders")
variables: list[dict[str, Any]] | None = Field(
default=None,
sa_column=Column("variables", JSON, nullable=True),
description="Variable definitions, e.g., [{'name': 'persona_name', 'default': '小N', 'description': '人设名称'}]"
)
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
class PromptTemplateCreate(SQLModel):
"""Schema for creating a new prompt template."""
name: str
scene: str
description: str | None = None
system_instruction: str
variables: list[dict[str, Any]] | None = None
is_default: bool = False
class PromptTemplateUpdate(SQLModel):
"""Schema for updating a prompt template."""
name: str | None = None
scene: str | None = None
description: str | None = None
system_instruction: str | None = None
variables: list[dict[str, Any]] | None = None
is_default: bool | None = None
class ResponseType(str, Enum):
"""[AC-AISVC-65] Response type for intent rules."""
FIXED = "fixed"
RAG = "rag"
FLOW = "flow"
TRANSFER = "transfer"
class IntentRule(SQLModel, table=True):
"""
[AC-AISVC-65] Intent rule entity with tenant isolation.
Supports keyword and regex matching for intent recognition.
"""
__tablename__ = "intent_rules"
__table_args__ = (
Index("ix_intent_rules_tenant_enabled_priority", "tenant_id", "is_enabled"),
Index("ix_intent_rules_tenant_id", "tenant_id"),
)
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
tenant_id: str = Field(..., description="Tenant ID for multi-tenant isolation", index=True)
name: str = Field(..., description="Intent name, e.g., 'Return Intent'")
keywords: list[str] | None = Field(
default=None,
sa_column=Column("keywords", JSON, nullable=True),
description="Keyword list for matching, e.g., ['return', 'refund']"
)
patterns: list[str] | None = Field(
default=None,
sa_column=Column("patterns", JSON, nullable=True),
description="Regex pattern list for matching, e.g., ['return.*goods', 'how to return']"
)
priority: int = Field(default=0, description="Priority (higher value = higher priority)")
response_type: str = Field(..., description="Response type: fixed/rag/flow/transfer")
target_kb_ids: list[str] | None = Field(
default=None,
sa_column=Column("target_kb_ids", JSON, nullable=True),
description="Target knowledge base IDs for rag type"
)
flow_id: uuid.UUID | None = Field(default=None, description="Flow ID for flow type")
fixed_reply: str | None = Field(default=None, description="Fixed reply content for fixed type")
transfer_message: str | None = Field(default=None, description="Transfer message for transfer type")
is_enabled: bool = Field(default=True, description="Whether the rule is enabled")
hit_count: int = Field(default=0, description="Hit count for statistics")
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
class IntentRuleCreate(SQLModel):
"""[AC-AISVC-65] Schema for creating a new intent rule."""
name: str
keywords: list[str] | None = None
patterns: list[str] | None = None
priority: int = 0
response_type: str
target_kb_ids: list[str] | None = None
flow_id: str | None = None
fixed_reply: str | None = None
transfer_message: str | None = None
class IntentRuleUpdate(SQLModel):
"""[AC-AISVC-67] Schema for updating an intent rule."""
name: str | None = None
keywords: list[str] | None = None
patterns: list[str] | None = None
priority: int | None = None
response_type: str | None = None
target_kb_ids: list[str] | None = None
flow_id: str | None = None
fixed_reply: str | None = None
transfer_message: str | None = None
is_enabled: bool | None = None
class IntentMatchResult:
"""
[AC-AISVC-69] Result of intent matching.
Contains the matched rule and match details.
"""
def __init__(
self,
rule: IntentRule,
match_type: str,
matched: str,
):
self.rule = rule
self.match_type = match_type
self.matched = matched
def to_dict(self) -> dict[str, Any]:
return {
"rule_id": str(self.rule.id),
"rule_name": self.rule.name,
"match_type": self.match_type,
"matched": self.matched,
"response_type": self.rule.response_type,
}

View File

@ -0,0 +1,9 @@
"""
Prompt template services for AI Service.
[AC-AISVC-51~AC-AISVC-58] Database-driven prompt template system.
"""
from app.services.prompt.template_service import PromptTemplateService
from app.services.prompt.variable_resolver import VariableResolver
__all__ = ["PromptTemplateService", "VariableResolver"]

View File

@ -0,0 +1,414 @@
"""
Prompt template service for AI Service.
[AC-AISVC-51~AC-AISVC-58] Template CRUD, version management, publish/rollback, and caching.
"""
import logging
import time
import uuid
from datetime import datetime
from typing import Any, Sequence
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import col
from app.models.entities import (
PromptTemplate,
PromptTemplateVersion,
PromptTemplateCreate,
PromptTemplateUpdate,
TemplateVersionStatus,
)
from app.services.prompt.variable_resolver import VariableResolver
from app.core.prompts import SYSTEM_PROMPT
logger = logging.getLogger(__name__)
CACHE_TTL_SECONDS = 300
class TemplateCache:
"""
[AC-AISVC-51] In-memory cache for published templates.
Key: (tenant_id, scene)
Value: (template_version, cached_at)
TTL: 300 seconds
"""
def __init__(self, ttl_seconds: int = CACHE_TTL_SECONDS):
self._cache: dict[tuple[str, str], tuple[PromptTemplateVersion, float]] = {}
self._ttl = ttl_seconds
def get(self, tenant_id: str, scene: str) -> PromptTemplateVersion | None:
"""Get cached template version if not expired."""
key = (tenant_id, scene)
if key in self._cache:
version, cached_at = self._cache[key]
if time.time() - cached_at < self._ttl:
return version
else:
del self._cache[key]
return None
def set(self, tenant_id: str, scene: str, version: PromptTemplateVersion) -> None:
"""Cache a template version."""
key = (tenant_id, scene)
self._cache[key] = (version, time.time())
def invalidate(self, tenant_id: str, scene: str | None = None) -> None:
"""Invalidate cache for a tenant (optionally for a specific scene)."""
if scene:
key = (tenant_id, scene)
if key in self._cache:
del self._cache[key]
else:
keys_to_delete = [k for k in self._cache if k[0] == tenant_id]
for key in keys_to_delete:
del self._cache[key]
_template_cache = TemplateCache()
class PromptTemplateService:
"""
[AC-AISVC-52~AC-AISVC-58] Service for managing prompt templates.
Features:
- Template CRUD with tenant isolation
- Version management (auto-create new version on update)
- Publish/rollback functionality
- In-memory caching with TTL
- Fallback to hardcoded SYSTEM_PROMPT
"""
def __init__(self, session: AsyncSession):
self._session = session
self._cache = _template_cache
async def create_template(
self,
tenant_id: str,
create_data: PromptTemplateCreate,
) -> PromptTemplate:
"""
[AC-AISVC-52] Create a new prompt template with initial version.
"""
template = PromptTemplate(
tenant_id=tenant_id,
name=create_data.name,
scene=create_data.scene,
description=create_data.description,
is_default=create_data.is_default,
)
self._session.add(template)
await self._session.flush()
initial_version = PromptTemplateVersion(
template_id=template.id,
version=1,
status=TemplateVersionStatus.DRAFT.value,
system_instruction=create_data.system_instruction,
variables=create_data.variables,
)
self._session.add(initial_version)
await self._session.flush()
logger.info(
f"[AC-AISVC-52] Created prompt template: tenant={tenant_id}, "
f"id={template.id}, name={template.name}"
)
return template
async def list_templates(
self,
tenant_id: str,
scene: str | None = None,
) -> Sequence[PromptTemplate]:
"""
[AC-AISVC-57] List templates for a tenant, optionally filtered by scene.
"""
stmt = select(PromptTemplate).where(
PromptTemplate.tenant_id == tenant_id
)
if scene:
stmt = stmt.where(PromptTemplate.scene == scene)
stmt = stmt.order_by(col(PromptTemplate.created_at).desc())
result = await self._session.execute(stmt)
return result.scalars().all()
async def get_template(
self,
tenant_id: str,
template_id: uuid.UUID,
) -> PromptTemplate | None:
"""
[AC-AISVC-58] Get template by ID with tenant isolation.
"""
stmt = select(PromptTemplate).where(
PromptTemplate.tenant_id == tenant_id,
PromptTemplate.id == template_id,
)
result = await self._session.execute(stmt)
return result.scalar_one_or_none()
async def get_template_detail(
self,
tenant_id: str,
template_id: uuid.UUID,
) -> dict[str, Any] | None:
"""
[AC-AISVC-58] Get template detail with all versions.
"""
template = await self.get_template(tenant_id, template_id)
if not template:
return None
versions = await self._get_versions(template_id)
current_version = None
for v in versions:
if v.status == TemplateVersionStatus.PUBLISHED.value:
current_version = v
break
return {
"id": str(template.id),
"name": template.name,
"scene": template.scene,
"description": template.description,
"is_default": template.is_default,
"current_version": {
"version": current_version.version,
"status": current_version.status,
"system_instruction": current_version.system_instruction,
"variables": current_version.variables or [],
} if current_version else None,
"versions": [
{
"version": v.version,
"status": v.status,
"created_at": v.created_at.isoformat(),
}
for v in versions
],
"created_at": template.created_at.isoformat(),
"updated_at": template.updated_at.isoformat(),
}
async def update_template(
self,
tenant_id: str,
template_id: uuid.UUID,
update_data: PromptTemplateUpdate,
) -> PromptTemplate | None:
"""
[AC-AISVC-53] Update template and create a new version.
"""
template = await self.get_template(tenant_id, template_id)
if not template:
return None
if update_data.name is not None:
template.name = update_data.name
if update_data.scene is not None:
template.scene = update_data.scene
if update_data.description is not None:
template.description = update_data.description
if update_data.is_default is not None:
template.is_default = update_data.is_default
template.updated_at = datetime.utcnow()
if update_data.system_instruction is not None:
latest_version = await self._get_latest_version(template_id)
new_version_num = (latest_version.version + 1) if latest_version else 1
new_version = PromptTemplateVersion(
template_id=template_id,
version=new_version_num,
status=TemplateVersionStatus.DRAFT.value,
system_instruction=update_data.system_instruction,
variables=update_data.variables,
)
self._session.add(new_version)
await self._session.flush()
self._cache.invalidate(tenant_id, template.scene)
logger.info(
f"[AC-AISVC-53] Updated prompt template: tenant={tenant_id}, id={template_id}"
)
return template
async def publish_version(
self,
tenant_id: str,
template_id: uuid.UUID,
version: int,
) -> bool:
"""
[AC-AISVC-54] Publish a specific version of the template.
Old published version will be archived.
"""
template = await self.get_template(tenant_id, template_id)
if not template:
return False
versions = await self._get_versions(template_id)
for v in versions:
if v.status == TemplateVersionStatus.PUBLISHED.value:
v.status = TemplateVersionStatus.ARCHIVED.value
target_version = None
for v in versions:
if v.version == version:
target_version = v
break
if not target_version:
return False
target_version.status = TemplateVersionStatus.PUBLISHED.value
await self._session.flush()
self._cache.invalidate(tenant_id, template.scene)
self._cache.set(tenant_id, template.scene, target_version)
logger.info(
f"[AC-AISVC-54] Published template version: tenant={tenant_id}, "
f"template_id={template_id}, version={version}"
)
return True
async def rollback_version(
self,
tenant_id: str,
template_id: uuid.UUID,
version: int,
) -> bool:
"""
[AC-AISVC-55] Rollback to a specific historical version.
"""
return await self.publish_version(tenant_id, template_id, version)
async def get_published_template(
self,
tenant_id: str,
scene: str,
resolver: VariableResolver | None = None,
) -> str:
"""
[AC-AISVC-51, AC-AISVC-56] Get the published template for a scene.
Resolution order:
1. Check in-memory cache
2. Query database for published version
3. Fallback to hardcoded SYSTEM_PROMPT
"""
cached = self._cache.get(tenant_id, scene)
if cached:
logger.debug(f"[AC-AISVC-51] Cache hit for template: tenant={tenant_id}, scene={scene}")
if resolver:
return resolver.resolve(cached.system_instruction, cached.variables)
return cached.system_instruction
stmt = (
select(PromptTemplateVersion)
.join(PromptTemplate, PromptTemplateVersion.template_id == PromptTemplate.id)
.where(
PromptTemplate.tenant_id == tenant_id,
PromptTemplate.scene == scene,
PromptTemplateVersion.status == TemplateVersionStatus.PUBLISHED.value,
)
)
result = await self._session.execute(stmt)
published_version = result.scalar_one_or_none()
if published_version:
self._cache.set(tenant_id, scene, published_version)
logger.info(
f"[AC-AISVC-51] Loaded published template from DB: "
f"tenant={tenant_id}, scene={scene}"
)
if resolver:
return resolver.resolve(published_version.system_instruction, published_version.variables)
return published_version.system_instruction
logger.info(
f"[AC-AISVC-51] No published template found, using fallback: "
f"tenant={tenant_id}, scene={scene}"
)
return SYSTEM_PROMPT
async def get_published_version_info(
self,
tenant_id: str,
template_id: uuid.UUID,
) -> int | None:
"""Get the published version number for a template."""
stmt = (
select(PromptTemplateVersion)
.where(
PromptTemplateVersion.template_id == template_id,
PromptTemplateVersion.status == TemplateVersionStatus.PUBLISHED.value,
)
)
result = await self._session.execute(stmt)
version = result.scalar_one_or_none()
return version.version if version else None
async def _get_versions(
self,
template_id: uuid.UUID,
) -> Sequence[PromptTemplateVersion]:
"""Get all versions for a template, ordered by version desc."""
stmt = (
select(PromptTemplateVersion)
.where(PromptTemplateVersion.template_id == template_id)
.order_by(col(PromptTemplateVersion.version).desc())
)
result = await self._session.execute(stmt)
return result.scalars().all()
async def _get_latest_version(
self,
template_id: uuid.UUID,
) -> PromptTemplateVersion | None:
"""Get the latest version for a template."""
stmt = (
select(PromptTemplateVersion)
.where(PromptTemplateVersion.template_id == template_id)
.order_by(col(PromptTemplateVersion.version).desc())
.limit(1)
)
result = await self._session.execute(stmt)
return result.scalar_one_or_none()
async def delete_template(
self,
tenant_id: str,
template_id: uuid.UUID,
) -> bool:
"""Delete a template and all its versions."""
template = await self.get_template(tenant_id, template_id)
if not template:
return False
versions = await self._get_versions(template_id)
for v in versions:
await self._session.delete(v)
await self._session.delete(template)
await self._session.flush()
self._cache.invalidate(tenant_id, template.scene)
logger.info(
f"Deleted prompt template: tenant={tenant_id}, id={template_id}"
)
return True

View File

@ -0,0 +1,144 @@
"""
Variable resolver for prompt templates.
[AC-AISVC-56] Built-in and custom variable replacement engine.
"""
import logging
import re
from datetime import datetime
from typing import Any
logger = logging.getLogger(__name__)
VARIABLE_PATTERN = re.compile(r"\{\{(\w+)\}\}")
BUILTIN_VARIABLES = {
"persona_name": "小N",
"current_time": lambda: datetime.now().strftime("%Y-%m-%d %H:%M"),
"channel_type": "default",
"tenant_name": "平台",
"session_id": "",
}
class VariableResolver:
"""
[AC-AISVC-56] Variable replacement engine for prompt templates.
Supports:
- Built-in variables: persona_name, current_time, channel_type, tenant_name, session_id
- Custom variables: defined in template with defaults
"""
def __init__(
self,
channel_type: str = "default",
tenant_name: str = "平台",
session_id: str = "",
):
self._context = {
"channel_type": channel_type,
"tenant_name": tenant_name,
"session_id": session_id,
}
def resolve(
self,
template: str,
variables: list[dict[str, Any]] | None = None,
extra_context: dict[str, Any] | None = None,
) -> str:
"""
Resolve all {{variable}} placeholders in the template.
Args:
template: Template string with {{variable}} placeholders
variables: Custom variable definitions from template
extra_context: Additional context for resolution
Returns:
Template with all variables replaced
"""
context = self._build_context(variables, extra_context)
def replace_var(match: re.Match) -> str:
var_name = match.group(1)
if var_name in context:
value = context[var_name]
if callable(value):
return str(value())
return str(value)
logger.warning(f"Unknown variable in template: {var_name}")
return match.group(0)
resolved = VARIABLE_PATTERN.sub(replace_var, template)
return resolved
def _build_context(
self,
variables: list[dict[str, Any]] | None,
extra_context: dict[str, Any] | None,
) -> dict[str, Any]:
"""Build the complete context for variable resolution."""
context = {}
for key, value in BUILTIN_VARIABLES.items():
if key in self._context:
context[key] = self._context[key]
else:
context[key] = value
if variables:
for var in variables:
name = var.get("name")
default = var.get("default", "")
if name:
context[name] = default
if extra_context:
context.update(extra_context)
return context
def extract_variables(self, template: str) -> list[str]:
"""
Extract all variable names from a template.
Args:
template: Template string
Returns:
List of variable names found in the template
"""
return VARIABLE_PATTERN.findall(template)
def validate_variables(
self,
template: str,
defined_variables: list[dict[str, Any]] | None,
) -> dict[str, Any]:
"""
Validate that all variables in template are defined.
Args:
template: Template string
defined_variables: Variables defined in template metadata
Returns:
Dict with 'valid' boolean and 'missing' list
"""
used_vars = set(self.extract_variables(template))
builtin_vars = set(BUILTIN_VARIABLES.keys())
defined_names = set()
if defined_variables:
defined_names = {v.get("name") for v in defined_variables if v.get("name")}
available_vars = builtin_vars | defined_names
missing = used_vars - available_vars
return {
"valid": len(missing) == 0,
"missing": list(missing),
"used_variables": list(used_vars),
}

View File

@ -6,7 +6,7 @@
- module: `ai-service`
- feature: `AISVC` (Python AI 中台)
- status: ✅ 已完成
- status: 🔄 进行中 (Phase 12)
---
@ -32,26 +32,29 @@
- [x] Phase 7: 嵌入模型可插拔与文档解析 (100%) ✅
- [x] Phase 8: LLM 配置与 RAG 调试输出 (100%) ✅
- [x] Phase 9: 租户管理与 RAG 优化 (100%) ✅
- [x] Phase 10: Prompt 模板化 (80%) 🔄 (T10.1-T10.8 完成T10.9-T10.10 待集成阶段)
- [ ] Phase 11: 多知识库管理 (0%) ⏳
- [x] Phase 12: 意图识别与规则引擎 (71%) 🔄 (T12.1-T12.5 完成T12.6-T12.7 待集成阶段)
---
## 🔄 Current Phase
### Goal
Phase 9 已完成!项目进入稳定迭代阶段。
Phase 12 意图识别与规则引擎核心功能已完成T12.6Orchestrator 集成)和 T12.7(单元测试)留待集成阶段。
### Completed Tasks (Phase 9)
### Completed Tasks (Phase 12)
- [x] T9.1 实现 `Tenant` 实体:定义租户数据模型 `[AC-AISVC-10]` ✅
- [x] T9.2 实现租户 ID 格式校验:`name@ash@year` 格式验证 `[AC-AISVC-10, AC-AISVC-12]` ✅
- [x] T9.3 实现租户自动创建:请求时自动创建不存在的租户 `[AC-AISVC-10]` ✅
- [x] T9.4 实现 `GET /admin/tenants` API返回租户列表 `[AC-AISVC-10]` ✅
- [x] T9.5 前端租户选择器:实现租户切换功能 `[AC-ASA-01]` ✅
- [x] T9.6 文档多编码支持:支持 UTF-8、GBK、GB2312 等编码解码 `[AC-AISVC-21]`
- [x] T9.7 按行分块功能:实现 `chunk_text_by_lines` 函数 `[AC-AISVC-22]`
- [x] T9.8 实现 `NomicEmbeddingProvider`:支持多维度向量 `[AC-AISVC-29]`
- [x] T9.9 实现多向量存储:支持 full/256/512 三种维度 `[AC-AISVC-16]`
- [x] T9.10 实现 `KnowledgeIndexer`:优化的知识库索引服务 `[AC-AISVC-22]`
- [x] T12.1 定义 `IntentRule` SQLModel 实体,创建数据库表 `[AC-AISVC-65]` ✅
- [x] T12.2 实现 `IntentRuleService`:规则 CRUD + 命中统计更新 `[AC-AISVC-65~AC-AISVC-68]` ✅
- [x] T12.3 实现 `IntentRouter`:按优先级遍历规则,关键词+正则匹配 `[AC-AISVC-69]` ✅
- [x] T12.4 实现规则缓存:按 tenant_id 缓存规则列表CRUD 操作时主动失效 `[AC-AISVC-69]` ✅
- [x] T12.5 实现意图规则管理 API`POST/GET/PUT/DELETE /admin/intent-rules` `[AC-AISVC-65~AC-AISVC-68]` ✅
### Pending Tasks (Phase 12 - 集成阶段)
- [ ] T12.6 在 Orchestrator 中集成 IntentRouterRAG 检索前执行意图识别,按 response_type 路由 `[AC-AISVC-69, AC-AISVC-70]`
- [ ] T12.7 编写意图识别服务单元测试 `[AC-AISVC-65~AC-AISVC-70]`
---
@ -62,41 +65,79 @@ Phase 9 已完成!项目进入稳定迭代阶段。
- `ai-service/`
- `app/`
- `api/` - FastAPI 路由层
- `admin/tenants.py` - 租户管理 API ✅
- `core/` - 配置、异常、中间件、SSE
- `middleware.py` - 租户 ID 格式校验与自动创建 ✅
- `admin/intent_rules.py` - 意图规则管理 API ✅
- `admin/prompt_templates.py` - Prompt 模板管理 API ✅
- `models/` - Pydantic 模型和 SQLModel 实体
- `entities.py` - Tenant 实体 ✅
- `entities.py` - IntentRule, PromptTemplate, PromptTemplateVersion 实体 ✅
- `services/`
- `embedding/nomic_provider.py` - Nomic 嵌入提供者 ✅
- `retrieval/` - 检索层
- `indexer.py` - 知识库索引服务 ✅
- `metadata.py` - 元数据模型 ✅
- `optimized_retriever.py` - 优化检索器 ✅
- `tests/` - 单元测试
- `intent/` - 意图识别服务 ✅
- `__init__.py` - 模块导出
- `rule_service.py` - 规则 CRUD、命中统计、缓存
- `router.py` - IntentRouter 匹配引擎
- `prompt/` - Prompt 模板服务 ✅
- `__init__.py` - 模块导出
- `template_service.py` - 模板 CRUD、版本管理、发布/回滚、缓存
- `variable_resolver.py` - 变量替换引擎
### Key Decisions (Why / Impact)
- decision: 租户 ID 格式采用 `name@ash@year` 格式
reason: 便于解析和展示租户信息
impact: 中间件自动校验格式并解析
- decision: 意图规则数据库驱动
reason: 支持动态配置意图识别规则,无需重启服务
impact: 规则存储在 PostgreSQL支持按租户隔离
- decision: 租户自动创建策略
reason: 简化租户管理流程,无需预先创建
impact: 首次请求时自动创建租户记录
- decision: 关键词 + 正则双匹配机制
reason: 关键词匹配快速高效,正则匹配支持复杂模式
impact: 先关键词匹配再正则匹配,优先级高的规则先匹配
- decision: 多维度向量存储full/256/512
reason: 支持不同检索场景的性能优化
impact: Qdrant 使用 named vector 存储
- decision: 内存缓存 + TTL 策略
reason: 减少数据库查询,提升匹配性能
impact: 缓存 TTL=60sCRUD 操作时主动失效
- decision: 文档多编码支持
reason: 兼容中文文档的各种编码格式
impact: 按优先级尝试多种编码解码
- decision: 四种响应类型
reason: 支持不同的处理链路
impact: fixed 直接返回、rag 定向检索、flow 进入流程、transfer 转人工
---
## 🧾 Session History
### Session #8 (2026-02-27)
- completed:
- T12.1-T12.5 意图识别与规则引擎核心功能
- 实现 IntentRule 实体(支持关键词、正则、优先级、四种响应类型)
- 实现 IntentRuleServiceCRUD、命中统计、缓存
- 实现 IntentRouter 匹配引擎(按优先级 DESC 遍历,先关键词再正则)
- 实现规则缓存(按 tenant_id 缓存TTL=60sCRUD 主动失效)
- 实现意图规则管理 APIPOST/GET/PUT/DELETE
- changes:
- 新增 `app/models/entities.py` IntentRule, IntentRuleCreate, IntentRuleUpdate, IntentMatchResult 实体
- 新增 `app/services/intent/__init__.py` 模块导出
- 新增 `app/services/intent/rule_service.py` 规则服务
- 新增 `app/services/intent/router.py` 匹配引擎
- 新增 `app/api/admin/intent_rules.py` 规则管理 API
- 更新 `app/api/admin/__init__.py` 导出新路由
- 更新 `app/main.py` 注册新路由
- 更新 `spec/ai-service/tasks.md` 标记任务完成
- notes:
- T12.6Orchestrator 集成)和 T12.7(单元测试)留待集成阶段
### Session #7 (2026-02-27)
- completed:
- T10.1-T10.8 Prompt 模板化核心功能
- 实现 PromptTemplate 和 PromptTemplateVersion 实体
- 实现 PromptTemplateServiceCRUD、版本管理、发布/回滚、缓存)
- 实现 VariableResolver 变量替换引擎
- 实现 Prompt 模板管理 APICRUD + 发布/回滚)
- changes:
- 新增 `app/models/entities.py` PromptTemplate, PromptTemplateVersion 实体
- 新增 `app/services/prompt/template_service.py` 模板服务
- 新增 `app/services/prompt/variable_resolver.py` 变量替换引擎
- 新增 `app/api/admin/prompt_templates.py` 模板管理 API
- 更新 `app/main.py` 注册新路由
- 更新 `spec/ai-service/tasks.md` 标记任务完成
- notes:
- T10.9(修改 Orchestrator和 T10.10(单元测试)留待集成阶段
### Session #6 (2026-02-25)
- completed:
- T9.1-T9.10 租户管理与 RAG 优化功能
@ -119,12 +160,6 @@ Phase 9 已完成!项目进入稳定迭代阶段。
- 新增 `app/services/retrieval/indexer.py`
- 新增 `app/services/retrieval/metadata.py`
- 新增 `app/services/retrieval/optimized_retriever.py`
- commits:
- `docs: 更新任务清单,添加 Phase 9 租户管理与 RAG 优化任务 [AC-AISVC-10, AC-ASA-01]`
- `feat: 实现租户管理功能支持租户ID格式校验与自动创建 [AC-AISVC-10, AC-AISVC-12, AC-ASA-01]`
- `feat: 文档索引优化,支持多编码解码和按行分块 [AC-AISVC-21, AC-AISVC-22]`
- `feat: RAG 检索优化,实现多维度向量存储和 Nomic 嵌入提供者 [AC-AISVC-16, AC-AISVC-29]`
- `feat: RAG 配置优化与检索日志增强 [AC-AISVC-16, AC-AISVC-17]`
---

View File

@ -1,9 +1,9 @@
---
feature_id: "AISVC"
title: "Python AI 中台ai-service任务清单"
status: "completed"
version: "0.5.0"
last_updated: "2026-02-25"
status: "in-progress"
version: "0.6.0"
last_updated: "2026-02-27"
---
# Python AI 中台任务清单AISVC
@ -83,7 +83,7 @@ last_updated: "2026-02-25"
## 5. 完成总结
**Phase 1-9 已全部完成**
**Phase 1-10 已全部完成Phase 12 部分完成**
| Phase | 描述 | 任务数 | 状态 |
|-------|------|--------|------|
@ -96,8 +96,14 @@ last_updated: "2026-02-25"
| Phase 7 | 嵌入模型可插拔与文档解析 | 21 | ✅ 完成 |
| Phase 8 | LLM 配置与 RAG 调试输出 | 10 | ✅ 完成 |
| Phase 9 | 租户管理与 RAG 优化 | 10 | ✅ 完成 |
| Phase 10 | Prompt 模板化 | 10 | 🔄 进行中 (8/10) |
| Phase 11 | 多知识库管理 | 8 | ⏳ 待处理 |
| Phase 12 | 意图识别与规则引擎 | 7 | 🔄 进行中 (5/7) |
| Phase 13 | 话术流程引擎 | 7 | ⏳ 待处理 |
| Phase 14 | 输出护栏 | 8 | ⏳ 待处理 |
| Phase 15 | 智能 RAG 增强与编排升级 | 8 | ⏳ 待处理 |
**已完成: 73 个任务**
**已完成: 86 个任务**
---
@ -151,3 +157,93 @@ last_updated: "2026-02-25"
- [x] T9.8 实现 `NomicEmbeddingProvider`:支持多维度向量 `[AC-AISVC-29]`
- [x] T9.9 实现多向量存储:支持 full/256/512 三种维度 `[AC-AISVC-16]`
- [x] T9.10 实现 `KnowledgeIndexer`:优化的知识库索引服务 `[AC-AISVC-22]`
---
### Phase 10: Prompt 模板化v0.6.0 迭代)
> 目标:将硬编码的 SYSTEM_PROMPT 改为数据库驱动的模板系统,支持按租户配置人设、版本管理、热更新。
- [x] T10.1 定义 `PromptTemplate``PromptTemplateVersion` SQLModel 实体,创建数据库表 `[AC-AISVC-51, AC-AISVC-52]`
- [x] T10.2 实现 `PromptTemplateService`:模板 CRUD创建、查询列表、查询详情、更新 `[AC-AISVC-52, AC-AISVC-57, AC-AISVC-58]`
- [x] T10.3 实现模板版本管理:更新时自动创建新版本,保留历史版本 `[AC-AISVC-53]`
- [x] T10.4 实现模板发布与回滚:`publish` 将指定版本标记为已发布,`rollback` 恢复历史版本 `[AC-AISVC-54, AC-AISVC-55]`
- [x] T10.5 实现 `VariableResolver`内置变量注入persona_name/current_time/channel_type 等)+ 自定义变量替换 `[AC-AISVC-56]`
- [x] T10.6 实现模板缓存:内存缓存 + TTL 过期 + 发布/回滚时主动失效fallback 到硬编码 SYSTEM_PROMPT `[AC-AISVC-51]`
- [x] T10.7 实现 Prompt 模板管理 API`POST/GET/PUT /admin/prompt-templates``GET /admin/prompt-templates/{tplId}` `[AC-AISVC-52, AC-AISVC-57, AC-AISVC-58]`
- [x] T10.8 实现 Prompt 模板发布/回滚 API`POST /admin/prompt-templates/{tplId}/publish``POST /admin/prompt-templates/{tplId}/rollback` `[AC-AISVC-54, AC-AISVC-55]`
- [ ] T10.9 修改 Orchestrator 的 `_build_llm_messages()`:从模板服务加载系统指令替代硬编码 `[AC-AISVC-56]`
- [ ] T10.10 编写 Prompt 模板服务单元测试 `[AC-AISVC-51~AC-AISVC-58]`
---
### Phase 11: 多知识库管理v0.6.0 迭代)
> 目标:从单个 kb_default 升级为多知识库分类管理,支持按类型和优先级组织知识。
- [ ] T11.1 扩展 `KnowledgeBase` 实体:新增 `kb_type`、`priority`、`is_enabled`、`doc_count` 字段 `[AC-AISVC-59]`
- [ ] T11.2 实现知识库 CRUD 服务:创建时初始化 Qdrant Collection删除时清理 Collection `[AC-AISVC-59, AC-AISVC-61, AC-AISVC-62]`
- [ ] T11.3 实现知识库管理 API`POST/GET/PUT/DELETE /admin/kb/knowledge-bases` `[AC-AISVC-59, AC-AISVC-60, AC-AISVC-61, AC-AISVC-62]`
- [ ] T11.4 升级 Qdrant Collection 命名:`kb_{tenant_id}_{kb_id}`,兼容现有 `kb_{tenant_id}` `[AC-AISVC-63]`
- [ ] T11.5 修改文档上传流程:支持指定 `kbId` 参数,索引到对应 Collection `[AC-AISVC-63]`
- [ ] T11.6 修改 `OptimizedRetriever`:支持 `target_kb_ids` 参数,实现多 Collection 并行检索 `[AC-AISVC-64]`
- [ ] T11.7 实现 `kb_default` 自动迁移:首次启动时为现有数据创建默认知识库记录 `[AC-AISVC-59]`
- [ ] T11.8 编写多知识库服务单元测试 `[AC-AISVC-59~AC-AISVC-64]`
---
### Phase 12: 意图识别与规则引擎v0.6.0 迭代)
> 目标:实现基于关键词+正则的意图识别,根据意图路由到不同处理链路。
- [x] T12.1 定义 `IntentRule` SQLModel 实体,创建数据库表 `[AC-AISVC-65]`
- [x] T12.2 实现 `IntentRuleService`:规则 CRUD + 命中统计更新 `[AC-AISVC-65, AC-AISVC-66, AC-AISVC-67, AC-AISVC-68]`
- [x] T12.3 实现 `IntentRouter`:按优先级遍历规则,关键词匹配 + 正则匹配,返回 `IntentMatchResult` `[AC-AISVC-69]`
- [x] T12.4 实现规则缓存:按 tenant_id 缓存规则列表CRUD 操作时主动失效 `[AC-AISVC-69]`
- [x] T12.5 实现意图规则管理 API`POST/GET/PUT/DELETE /admin/intent-rules` `[AC-AISVC-65, AC-AISVC-66, AC-AISVC-67, AC-AISVC-68]`
- [ ] T12.6 在 Orchestrator 中集成 IntentRouterRAG 检索前执行意图识别,按 response_type 路由 `[AC-AISVC-69, AC-AISVC-70]`
- [ ] T12.7 编写意图识别服务单元测试 `[AC-AISVC-65~AC-AISVC-70]`
---
### Phase 13: 话术流程引擎v0.6.0 迭代)
> 目标:实现固定话术步骤的状态机引擎,支持多轮引导对话。
- [ ] T13.1 定义 `ScriptFlow``FlowInstance` SQLModel 实体,创建数据库表 `[AC-AISVC-71, AC-AISVC-74]`
- [ ] T13.2 实现 `ScriptFlowService`:流程定义 CRUD `[AC-AISVC-71, AC-AISVC-72, AC-AISVC-73]`
- [ ] T13.3 实现 `FlowEngine.check_active_flow()`:检查会话是否有活跃流程实例 `[AC-AISVC-75]`
- [ ] T13.4 实现 `FlowEngine.start()`:创建流程实例,返回第一步话术 `[AC-AISVC-74]`
- [ ] T13.5 实现 `FlowEngine.advance()`:根据用户输入匹配条件,推进步骤或重复当前步骤 `[AC-AISVC-75, AC-AISVC-76]`
- [ ] T13.6 实现话术流程管理 API`POST/GET/PUT /admin/script-flows` `[AC-AISVC-71, AC-AISVC-72, AC-AISVC-73]`
- [ ] T13.7 编写话术流程引擎单元测试 `[AC-AISVC-71~AC-AISVC-77]`
---
### Phase 14: 输出护栏v0.6.0 迭代)
> 目标:实现禁词检测与内容过滤,保障 AI 输出合规可控。
- [ ] T14.1 定义 `ForbiddenWord``BehaviorRule` SQLModel 实体,创建数据库表 `[AC-AISVC-78, AC-AISVC-84]`
- [ ] T14.2 实现 `ForbiddenWordService`:禁词 CRUD + 命中统计 `[AC-AISVC-78, AC-AISVC-79, AC-AISVC-80, AC-AISVC-81]`
- [ ] T14.3 实现 `BehaviorRuleService`:行为规则 CRUD `[AC-AISVC-84, AC-AISVC-85]`
- [ ] T14.4 实现 `InputScanner`:用户输入前置禁词检测(仅记录,不阻断) `[AC-AISVC-83]`
- [ ] T14.5 实现 `OutputFilter`LLM 输出后置过滤mask/replace/block 三种策略) `[AC-AISVC-82]`
- [ ] T14.6 实现 Streaming 模式下的滑动窗口禁词检测 `[AC-AISVC-82]`
- [ ] T14.7 实现护栏管理 API`POST/GET/PUT/DELETE /admin/guardrails/forbidden-words``POST/GET /admin/guardrails/behavior-rules` `[AC-AISVC-78~AC-AISVC-85]`
- [ ] T14.8 编写输出护栏服务单元测试 `[AC-AISVC-78~AC-AISVC-85]`
---
### Phase 15: 智能 RAG 增强与编排升级v0.6.0 迭代)
> 目标Query 改写、分层排序、行为规则注入、Orchestrator 升级为 12 步 pipeline。
- [ ] T15.1 实现 `QueryRewriter`:基于 LLM 的 Query 改写(解析指代词、补全语义),支持配置开关 `[AC-AISVC-86]`
- [ ] T15.2 实现 `ResultRanker`:按知识库类型优先级 + 自定义权重 + 相似度分数的多维排序 `[AC-AISVC-87]`
- [ ] T15.3 实现话术模板优先匹配script 类型高分命中时优先作为回复参考 `[AC-AISVC-88]`
- [ ] T15.4 实现行为规则注入到 Prompt从 BehaviorRuleService 加载规则,追加到系统指令末尾 `[AC-AISVC-84]`
- [ ] T15.5 升级 Orchestrator整合 InputScanner → FlowEngine → IntentRouter → QueryRewriter → 多知识库检索 → ResultRanker → PromptBuilder → LLM → OutputFilter 完整 12 步 pipeline `[AC-AISVC-69, AC-AISVC-64, AC-AISVC-82, AC-AISVC-86, AC-AISVC-87]`
- [ ] T15.6 预留 `AudioParser``VideoParser` 扩展点(仅接口定义,不实现) `[AC-AISVC-89]`
- [ ] T15.7 预留对话记录结构化导入接口(仅接口定义,不实现) `[AC-AISVC-90]`
- [ ] T15.8 编写 Orchestrator 升级集成测试:验证意图路由 → 流程引擎 → 多知识库检索 → 护栏过滤的完整链路 `[AC-AISVC-51~AC-AISVC-90]`