-
成长档案
+
近期实修回顾
周期报告
求助卡片
分享草稿
diff --git a/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts b/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts
index 6f6a0da..31e4a58 100644
--- a/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts
+++ b/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts
@@ -489,13 +489,9 @@ export interface TopicSummaryRecord {
id: number;
topicSessionId: number;
userId: number;
+ schemaVersion: number;
summary: string;
- mainEvents?: string | null;
- emotions?: string | null;
- bodyFeelings?: string | null;
- beliefs?: string | null;
- recommendedHomework?: string | null;
- insights?: string | null;
+ currentFocus?: string | null;
nextObservation?: string | null;
modelName?: string | null;
status: string;
@@ -511,14 +507,10 @@ export interface TopicSummaryRecord {
export interface UserGrowthProfileRecord {
id: number;
userId: number;
- profileText: string;
- recurringTopics?: string | null;
- commonEmotions?: string | null;
- bodyPatterns?: string | null;
- relationPatterns?: string | null;
- homeworkDone?: string | null;
- effectiveHomework?: string | null;
- recentProgress?: string | null;
+ schemaVersion: number;
+ reviewText: string;
+ currentFocus?: string | null;
+ sourceSummaryIds: number[];
lastTopicSummaryId?: number | null;
updatedBy: string;
createdAt: string;
@@ -528,6 +520,7 @@ export interface UserGrowthProfileRecord {
export interface PeriodicReportRecord {
id: number;
userId: number;
+ schemaVersion: number;
reportType: "weekly" | "monthly" | "stage" | string;
reportTypeLabel: string;
periodStart: string;
@@ -561,7 +554,6 @@ export interface TopicSessionRecord {
tokenInput: number;
tokenOutput: number;
quotaDeducted: boolean;
- recommendedHomework?: string | null;
startedAt: string;
endedAt?: string | null;
createdAt?: string | null;
diff --git a/ai_knowledge_base_v2/apps/backend/alembic/versions/0014_entitlements_topics.py b/ai_knowledge_base_v2/apps/backend/alembic/versions/0014_entitlements_topics.py
index 56f7968..625fedc 100644
--- a/ai_knowledge_base_v2/apps/backend/alembic/versions/0014_entitlements_topics.py
+++ b/ai_knowledge_base_v2/apps/backend/alembic/versions/0014_entitlements_topics.py
@@ -174,7 +174,7 @@ def _seed_default_plans() -> None:
{
"name": "五个月深度陪伴版",
"plan_type": "deep",
- "description": "支持长期成长档案、阶段报告和更高主题会话额度。",
+ "description": "支持近期实修回顾、周期实修回顾和更高主题会话额度。",
"validity_days": 150,
"monthly_topic_limit": 90,
"enable_growth_profile": 1,
diff --git a/ai_knowledge_base_v2/apps/backend/alembic/versions/0025_recent_practice_review.py b/ai_knowledge_base_v2/apps/backend/alembic/versions/0025_recent_practice_review.py
new file mode 100644
index 0000000..82af4f7
--- /dev/null
+++ b/ai_knowledge_base_v2/apps/backend/alembic/versions/0025_recent_practice_review.py
@@ -0,0 +1,70 @@
+"""replace long-term growth profiling with recent practice review
+
+Revision ID: 0025_recent_practice_review
+Revises: 0024_topic_settlement_jobs
+"""
+
+from __future__ import annotations
+
+from alembic import op
+import sqlalchemy as sa
+
+
+revision = "0025_recent_practice_review"
+down_revision = "0024_topic_settlement_jobs"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ op.add_column(
+ "sys_topic_summary",
+ sa.Column("schema_version", sa.Integer(), nullable=False, server_default="1"),
+ )
+ op.alter_column("sys_topic_summary", "schema_version", server_default="2")
+
+ op.add_column(
+ "sys_user_growth_profile",
+ sa.Column("schema_version", sa.Integer(), nullable=False, server_default="1"),
+ )
+ op.add_column("sys_user_growth_profile", sa.Column("recent_review", sa.Text(), nullable=True))
+ op.add_column("sys_user_growth_profile", sa.Column("current_focus", sa.Text(), nullable=True))
+ op.add_column("sys_user_growth_profile", sa.Column("source_summary_ids", sa.Text(), nullable=True))
+ op.alter_column("sys_user_growth_profile", "schema_version", server_default="2")
+
+ op.add_column(
+ "sys_periodic_report",
+ sa.Column("schema_version", sa.Integer(), nullable=False, server_default="1"),
+ )
+ op.alter_column("sys_periodic_report", "schema_version", server_default="2")
+
+ # 仅替换系统初始深度版的旧说明,避免覆盖管理员已经自定义的文案。
+ op.execute(
+ sa.text(
+ "UPDATE sys_entitlement_plan "
+ "SET description = :new_description "
+ "WHERE plan_type = 'deep' AND description = :old_description"
+ ).bindparams(
+ new_description="支持近期实修回顾、周期实修回顾和更高主题会话额度。",
+ old_description="支持长期成长档案、阶段报告和更高主题会话额度。",
+ )
+ )
+
+
+def downgrade() -> None:
+ op.execute(
+ sa.text(
+ "UPDATE sys_entitlement_plan "
+ "SET description = :old_description "
+ "WHERE plan_type = 'deep' AND description = :new_description"
+ ).bindparams(
+ old_description="支持长期成长档案、阶段报告和更高主题会话额度。",
+ new_description="支持近期实修回顾、周期实修回顾和更高主题会话额度。",
+ )
+ )
+ op.drop_column("sys_periodic_report", "schema_version")
+ op.drop_column("sys_user_growth_profile", "source_summary_ids")
+ op.drop_column("sys_user_growth_profile", "current_focus")
+ op.drop_column("sys_user_growth_profile", "recent_review")
+ op.drop_column("sys_user_growth_profile", "schema_version")
+ op.drop_column("sys_topic_summary", "schema_version")
diff --git a/ai_knowledge_base_v2/apps/backend/app/api/user.py b/ai_knowledge_base_v2/apps/backend/app/api/user.py
index 906ab3e..95a268b 100644
--- a/ai_knowledge_base_v2/apps/backend/app/api/user.py
+++ b/ai_knowledge_base_v2/apps/backend/app/api/user.py
@@ -45,8 +45,9 @@ def growth_profile(
{
"id": item.id,
"topicSessionId": item.topic_session_id,
+ "schemaVersion": item.schema_version,
"summary": item.summary,
- "recommendedHomework": item.recommended_homework,
+ "currentFocus": item.main_events,
"nextObservation": item.next_observation,
"status": item.status,
"errorMessage": item.error_message,
diff --git a/ai_knowledge_base_v2/apps/backend/app/models/growth.py b/ai_knowledge_base_v2/apps/backend/app/models/growth.py
index 13b7395..b206c0c 100644
--- a/ai_knowledge_base_v2/apps/backend/app/models/growth.py
+++ b/ai_knowledge_base_v2/apps/backend/app/models/growth.py
@@ -21,6 +21,7 @@ class TopicSummary(Base):
id: Mapped[int] = mapped_column(PRIMARY_KEY_TYPE, primary_key=True, autoincrement=True)
topic_session_id: Mapped[int] = mapped_column(ForeignKey("sys_topic_session.id"), index=True, nullable=False)
user_id: Mapped[int] = mapped_column(ForeignKey("sys_user.id"), index=True, nullable=False)
+ schema_version: Mapped[int] = mapped_column(Integer, default=2, nullable=False)
summary: Mapped[str] = mapped_column(Text, nullable=False)
main_events: Mapped[str | None] = mapped_column(Text, nullable=True)
emotions: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -50,6 +51,11 @@ class UserGrowthProfile(Base):
id: Mapped[int] = mapped_column(PRIMARY_KEY_TYPE, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("sys_user.id"), index=True, nullable=False)
+ schema_version: Mapped[int] = mapped_column(Integer, default=2, nullable=False)
+ recent_review: Mapped[str | None] = mapped_column(Text, nullable=True)
+ current_focus: Mapped[str | None] = mapped_column(Text, nullable=True)
+ source_summary_ids: Mapped[str | None] = mapped_column(Text, nullable=True)
+ # 以下字段仅为兼容历史数据保留。V2 近期实修回顾不再生成、展示或注入这些画像字段。
profile_text: Mapped[str] = mapped_column(Text, default="", nullable=False)
recurring_topics: Mapped[str | None] = mapped_column(Text, nullable=True)
common_emotions: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -125,6 +131,7 @@ class PeriodicReport(Base):
id: Mapped[int] = mapped_column(PRIMARY_KEY_TYPE, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("sys_user.id"), index=True, nullable=False)
+ schema_version: Mapped[int] = mapped_column(Integer, default=2, nullable=False)
report_type: Mapped[str] = mapped_column(String(30), nullable=False)
period_start: Mapped[datetime] = mapped_column(DateTime, nullable=False)
period_end: Mapped[datetime] = mapped_column(DateTime, nullable=False)
diff --git a/ai_knowledge_base_v2/apps/backend/app/services/agent_debug_service.py b/ai_knowledge_base_v2/apps/backend/app/services/agent_debug_service.py
index 4c6616a..8af23ad 100644
--- a/ai_knowledge_base_v2/apps/backend/app/services/agent_debug_service.py
+++ b/ai_knowledge_base_v2/apps/backend/app/services/agent_debug_service.py
@@ -163,7 +163,7 @@ class AgentDebugService:
"userId": user.id,
"userName": user.name,
"entitlement": entitlement_dict(entitlement),
- "growthProfileUsed": bool(growth_context),
+ "recentPracticeReviewUsed": bool(growth_context),
"productContextUsed": True,
"topic": {
"id": topic.id,
diff --git a/ai_knowledge_base_v2/apps/backend/app/services/chat_service.py b/ai_knowledge_base_v2/apps/backend/app/services/chat_service.py
index 5926cf5..96cfa49 100644
--- a/ai_knowledge_base_v2/apps/backend/app/services/chat_service.py
+++ b/ai_knowledge_base_v2/apps/backend/app/services/chat_service.py
@@ -130,7 +130,7 @@ class ChatService:
growth_context = GrowthProfileService.prompt_context(db, user) if entitlement.enable_growth_profile else None
if growth_context:
context_trace = list(context_trace or [])
- context_trace.append({"tool": "load_growth_profile", "status": "success", "count": 1})
+ context_trace.append({"tool": "load_recent_practice_review", "status": "success", "count": 1})
topic_context, topic_summary_used = TopicSessionService.prompt_context(db, topic)
product_context = entitlement_prompt_context(entitlement)
context_trace = list(context_trace or [])
diff --git a/ai_knowledge_base_v2/apps/backend/app/services/chat_stream_service.py b/ai_knowledge_base_v2/apps/backend/app/services/chat_stream_service.py
index 920f7cf..d9c8a3a 100644
--- a/ai_knowledge_base_v2/apps/backend/app/services/chat_stream_service.py
+++ b/ai_knowledge_base_v2/apps/backend/app/services/chat_stream_service.py
@@ -79,7 +79,7 @@ class ChatStreamService:
growth_context = GrowthProfileService.prompt_context(db, user) if entitlement.enable_growth_profile else None
if growth_context:
context_trace = list(context_trace or [])
- context_trace.append({"tool": "load_growth_profile", "status": "success", "count": 1})
+ context_trace.append({"tool": "load_recent_practice_review", "status": "success", "count": 1})
topic_context, topic_summary_used = TopicSessionService.prompt_context(db, topic)
product_context = entitlement_prompt_context(entitlement)
context_trace = _append_runtime_context_trace(
@@ -262,7 +262,7 @@ class ChatStreamService:
growth_context = GrowthProfileService.prompt_context(db, user) if entitlement.enable_growth_profile else None
if growth_context:
context_trace = list(context_trace or [])
- context_trace.append({"tool": "load_growth_profile", "status": "success", "count": 1})
+ context_trace.append({"tool": "load_recent_practice_review", "status": "success", "count": 1})
topic_context, topic_summary_used = TopicSessionService.prompt_context(db, topic)
product_context = entitlement_prompt_context(entitlement)
context_trace = _append_runtime_context_trace(
diff --git a/ai_knowledge_base_v2/apps/backend/app/services/growth_profile_service.py b/ai_knowledge_base_v2/apps/backend/app/services/growth_profile_service.py
index 5d306a3..0267a6f 100644
--- a/ai_knowledge_base_v2/apps/backend/app/services/growth_profile_service.py
+++ b/ai_knowledge_base_v2/apps/backend/app/services/growth_profile_service.py
@@ -2,7 +2,7 @@ from __future__ import annotations
import json
import re
-from datetime import UTC, datetime
+from datetime import UTC, datetime, timedelta
from typing import Any
from fastapi import HTTPException, status
@@ -18,6 +18,11 @@ from app.services.external_errors import ExternalServiceError
from app.services.tracked_generation_service import TrackedGenerationService
+RECENT_REVIEW_SCHEMA_VERSION = 2
+RECENT_REVIEW_DAYS = 30
+RECENT_REVIEW_TOPIC_LIMIT = 10
+
+
class GrowthProfileService:
@staticmethod
def finish_active_topic(db: Session, *, user: User, session: ChatSession, force: bool = False) -> dict:
@@ -117,7 +122,7 @@ class GrowthProfileService:
def process_topic_settlement(db: Session, *, summary: TopicSummary) -> tuple[TopicSummary, UserGrowthProfile | None]:
topic = db.get(TopicSession, summary.topic_session_id)
# Redis 失效时允许多个进程依靠数据库继续消费;用户行锁保证同一学员的
- # 多个主题不会并发覆盖长期成长档案。
+ # 多个主题不会并发覆盖近期实修回顾。
user = db.scalar(select(User).where(User.id == summary.user_id).with_for_update())
if topic is None or user is None or user.is_deleted:
raise ValueError("主题或用户不存在")
@@ -129,7 +134,8 @@ class GrowthProfileService:
process_job=True,
allow_fallback=False,
)
- topic.recommended_homework = generated.recommended_homework
+ # V2 不再固化或展示“建议功课”,历史列仅保留兼容。
+ topic.recommended_homework = None
db.add(topic)
entitlement = EntitlementService.active_entitlement(db, user)
profile = None
@@ -148,6 +154,8 @@ class GrowthProfileService:
allow_fallback: bool = True,
) -> TopicSummary:
existing = db.scalar(select(TopicSummary).where(TopicSummary.topic_session_id == topic.id))
+ if existing is not None and existing.schema_version < RECENT_REVIEW_SCHEMA_VERSION:
+ force = True
if existing is not None and not force:
if existing.status in {"success", "fallback"}:
return existing
@@ -165,8 +173,9 @@ class GrowthProfileService:
)
if not messages:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="当前主题还没有可沉淀的对话内容")
- conversation = _messages_text(messages)
- prompt = _topic_summary_prompt(topic, conversation)
+ user_content = _messages_text(messages, roles={"user"})
+ assistant_context = _messages_text(messages, roles={"assistant"})
+ prompt = _topic_summary_prompt(topic, user_content, assistant_context)
model_name = None
try:
completion = TrackedGenerationService.generate(
@@ -178,19 +187,20 @@ class GrowthProfileService:
raw = completion.answer
model_name = completion.model_name
parsed = _parse_summary_json(raw)
- data = parsed if parsed and "summary" in parsed else _fallback_summary(topic, conversation, raw)
+ data = parsed if parsed and "summary" in parsed else _fallback_summary(topic, user_content, raw)
status_value = "success"
error_message = None
except ExternalServiceError as exc:
if not allow_fallback:
raise
- data = _fallback_summary(topic, conversation, "")
+ data = _fallback_summary(topic, user_content, "")
status_value = "fallback"
error_message = str(exc)
if existing is None:
existing = TopicSummary(topic_session_id=topic.id, user_id=user.id)
_apply_summary(existing, data)
+ existing.schema_version = RECENT_REVIEW_SCHEMA_VERSION
existing.model_name = model_name
existing.status = status_value
existing.error_message = error_message
@@ -206,7 +216,11 @@ class GrowthProfileService:
@staticmethod
def update_growth_profile(db: Session, *, user: User, topic_summary: TopicSummary) -> UserGrowthProfile:
profile = db.scalar(select(UserGrowthProfile).where(UserGrowthProfile.user_id == user.id))
- if profile is not None and profile.last_topic_summary_id == topic_summary.id:
+ if (
+ profile is not None
+ and profile.schema_version >= RECENT_REVIEW_SCHEMA_VERSION
+ and profile.last_topic_summary_id == topic_summary.id
+ ):
return profile
before = growth_profile_dict(profile) if profile is not None else None
if profile is None:
@@ -214,7 +228,8 @@ class GrowthProfileService:
db.add(profile)
db.flush()
- prompt = _growth_profile_prompt(profile, topic_summary)
+ summaries = _recent_review_summaries(db, user_id=user.id)
+ prompt = _recent_review_prompt(summaries)
try:
completion = TrackedGenerationService.generate(
db,
@@ -224,11 +239,11 @@ class GrowthProfileService:
)
raw = completion.answer
parsed = _parse_summary_json(raw)
- data = parsed if parsed and "profileText" in parsed else _fallback_profile(profile, topic_summary, raw)
+ data = parsed if parsed and "reviewText" in parsed else _fallback_recent_review(summaries, raw)
except ExternalServiceError:
- data = _fallback_profile(profile, topic_summary, "")
+ data = _fallback_recent_review(summaries, "")
- _apply_profile(profile, data)
+ _apply_recent_review(profile, data, summaries)
profile.last_topic_summary_id = topic_summary.id
profile.updated_by = "system"
db.add(profile)
@@ -241,7 +256,7 @@ class GrowthProfileService:
topic_summary_id=topic_summary.id,
before_json=json.dumps(before, ensure_ascii=False, default=str) if before else None,
after_json=json.dumps(after, ensure_ascii=False, default=str),
- reason="topic_summary",
+ reason="recent_practice_review_v2",
)
)
return profile
@@ -255,7 +270,10 @@ class GrowthProfileService:
return list(
db.scalars(
select(TopicSummary)
- .where(TopicSummary.user_id == user_id)
+ .where(
+ TopicSummary.user_id == user_id,
+ TopicSummary.schema_version == RECENT_REVIEW_SCHEMA_VERSION,
+ )
.order_by(TopicSummary.generated_at.desc(), TopicSummary.id.desc())
.limit(limit)
)
@@ -264,13 +282,20 @@ class GrowthProfileService:
@staticmethod
def prompt_context(db: Session, user: User) -> str | None:
profile = GrowthProfileService.get_growth_profile(db, user.id)
- if profile is None or not profile.profile_text.strip():
+ if (
+ profile is None
+ or profile.schema_version < RECENT_REVIEW_SCHEMA_VERSION
+ or not (profile.recent_review or "").strip()
+ ):
return None
+ focus = (profile.current_focus or "").strip()
return (
- "[长期成长档案]\n"
- "以下是用户跨主题沉淀出的简要成长档案,只能用于理解用户长期模式和延续陪伴,"
- "不能替代本轮可靠知识,也不能臆造课程内容。\n"
- f"{_limit(profile.profile_text, 1800)}"
+ "[近期实修回顾]\n"
+ "以下内容只用于保持近期对话连续性。必须优先依据用户本轮表达,不得把历史回顾当成人格、"
+ "情绪、身体或关系模式,不得据此评价成长结果,也不能替代本轮可靠知识。\n"
+ f"近期回顾:{_limit(profile.recent_review or '', 1400)}\n"
+ + (f"近期关注:{_limit(focus, 600)}\n" if focus else "")
+ + "回答时帮助用户回到当前问题和当下,不要主动复述或强化历史标签。"
)
@@ -286,7 +311,6 @@ def topic_dict(topic: TopicSession) -> dict:
"tokenInput": topic.token_input,
"tokenOutput": topic.token_output,
"quotaDeducted": bool(topic.quota_deducted),
- "recommendedHomework": topic.recommended_homework,
"startedAt": topic.started_at,
"endedAt": topic.ended_at,
"createdAt": topic.created_at,
@@ -294,18 +318,16 @@ def topic_dict(topic: TopicSession) -> dict:
}
-def topic_summary_dict(summary: TopicSummary) -> dict:
+def topic_summary_dict(summary: TopicSummary) -> dict | None:
+ if summary.schema_version < RECENT_REVIEW_SCHEMA_VERSION:
+ return None
return {
"id": summary.id,
"topicSessionId": summary.topic_session_id,
"userId": summary.user_id,
+ "schemaVersion": summary.schema_version,
"summary": summary.summary,
- "mainEvents": summary.main_events,
- "emotions": summary.emotions,
- "bodyFeelings": summary.body_feelings,
- "beliefs": summary.beliefs,
- "recommendedHomework": summary.recommended_homework,
- "insights": summary.insights,
+ "currentFocus": summary.main_events,
"nextObservation": summary.next_observation,
"modelName": summary.model_name,
"status": summary.status,
@@ -320,19 +342,15 @@ def topic_summary_dict(summary: TopicSummary) -> dict:
def growth_profile_dict(profile: UserGrowthProfile | None) -> dict | None:
- if profile is None:
+ if profile is None or profile.schema_version < RECENT_REVIEW_SCHEMA_VERSION:
return None
return {
"id": profile.id,
"userId": profile.user_id,
- "profileText": profile.profile_text,
- "recurringTopics": profile.recurring_topics,
- "commonEmotions": profile.common_emotions,
- "bodyPatterns": profile.body_patterns,
- "relationPatterns": profile.relation_patterns,
- "homeworkDone": profile.homework_done,
- "effectiveHomework": profile.effective_homework,
- "recentProgress": profile.recent_progress,
+ "schemaVersion": profile.schema_version,
+ "reviewText": profile.recent_review or "",
+ "currentFocus": profile.current_focus,
+ "sourceSummaryIds": _json_int_list(profile.source_summary_ids),
"lastTopicSummaryId": profile.last_topic_summary_id,
"updatedBy": profile.updated_by,
"createdAt": profile.created_at,
@@ -342,78 +360,89 @@ def growth_profile_dict(profile: UserGrowthProfile | None) -> dict | None:
def _apply_summary(summary: TopicSummary, data: dict[str, Any]) -> None:
summary.summary = _field(data, "summary", "本主题已沉淀。", 4000)
- summary.main_events = _field(data, "mainEvents", "", 2000) or None
- summary.emotions = _field(data, "emotions", "", 1000) or None
- summary.body_feelings = _field(data, "bodyFeelings", "", 1000) or None
- summary.beliefs = _field(data, "beliefs", "", 1000) or None
- summary.recommended_homework = _field(data, "recommendedHomework", "", 1500) or None
- summary.insights = _field(data, "insights", "", 1500) or None
+ summary.main_events = _field(data, "currentFocus", "", 1200) or None
summary.next_observation = _field(data, "nextObservation", "", 1500) or None
+ summary.emotions = None
+ summary.body_feelings = None
+ summary.beliefs = None
+ summary.recommended_homework = None
+ summary.insights = None
-def _apply_profile(profile: UserGrowthProfile, data: dict[str, Any]) -> None:
- profile.profile_text = _field(data, "profileText", profile.profile_text or "暂无成长档案。", 5000)
- profile.recurring_topics = _field(data, "recurringTopics", "", 2000) or profile.recurring_topics
- profile.common_emotions = _field(data, "commonEmotions", "", 1500) or profile.common_emotions
- profile.body_patterns = _field(data, "bodyPatterns", "", 1500) or profile.body_patterns
- profile.relation_patterns = _field(data, "relationPatterns", "", 1500) or profile.relation_patterns
- profile.homework_done = _field(data, "homeworkDone", "", 1500) or profile.homework_done
- profile.effective_homework = _field(data, "effectiveHomework", "", 1500) or profile.effective_homework
- profile.recent_progress = _field(data, "recentProgress", "", 1500) or profile.recent_progress
+def _apply_recent_review(
+ profile: UserGrowthProfile,
+ data: dict[str, Any],
+ summaries: list[TopicSummary],
+) -> None:
+ profile.schema_version = RECENT_REVIEW_SCHEMA_VERSION
+ profile.recent_review = _field(data, "reviewText", "近期还没有可回顾的主题。", 3000)
+ profile.current_focus = _field(data, "currentFocus", "", 1000) or None
+ profile.source_summary_ids = json.dumps([int(item.id) for item in summaries], ensure_ascii=False)
+ # 主动清空旧画像字段,保证后续任何兼容代码也不会继续读取标签化内容。
+ profile.profile_text = ""
+ profile.recurring_topics = None
+ profile.common_emotions = None
+ profile.body_patterns = None
+ profile.relation_patterns = None
+ profile.homework_done = None
+ profile.effective_homework = None
+ profile.recent_progress = None
-def _topic_summary_prompt(topic: TopicSession, conversation: str) -> str:
+def _topic_summary_prompt(topic: TopicSession, user_content: str, assistant_context: str) -> str:
return (
- "你是大本营千问千答的主题沉淀助手。请把一个主题会话总结为结构化 JSON,"
- "只依据对话内容,不要扩展课程知识,不要评价用户人格。输出字段:"
- "summary, mainEvents, emotions, bodyFeelings, beliefs, recommendedHomework, insights, nextObservation。"
- "内容要偏“回到当下、回到自身、觉察情绪和感受、如是释放”,不要给很多术层面的建议。\n\n"
- f"主题标题:{topic.title}\n核心问题:{topic.core_question}\n\n对话:\n{_limit(conversation, 12000)}"
+ "你是大本营千问千答的近期主题回顾助手。请输出结构化 JSON,字段仅包含:"
+ "summary, currentFocus, nextObservation。"
+ "用户原话是唯一可以形成用户结论的证据;AI回复只用于理解上下文,不能成为用户特征或结论。"
+ "只描述本次明确谈到的内容和当下关注,不分析人格、潜意识、情绪模式、身体模式、关系模式、"
+ "成长阶段、功课效果或近期变化,不评分、不贴标签、不扩展课程知识。"
+ "nextObservation 最多一句,使用‘可以继续留意……’的开放表达,不设目标、不追求结果。\n\n"
+ f"主题标题:{topic.title}\n核心问题:{topic.core_question}\n\n"
+ f"用户原话:\n{_limit(user_content, 9000)}\n\n"
+ f"AI回复(仅作上下文参考):\n{_limit(assistant_context, 3000)}"
)
-def _growth_profile_prompt(profile: UserGrowthProfile, summary: TopicSummary) -> str:
- current = profile.profile_text or "暂无"
+def _recent_review_prompt(summaries: list[TopicSummary]) -> str:
+ evidence = [
+ {
+ "summaryId": item.id,
+ "summary": item.summary,
+ "currentFocus": item.main_events,
+ "nextObservation": item.next_observation,
+ "generatedAt": item.generated_at,
+ }
+ for item in summaries
+ ]
return (
- "你是大本营千问千答的长期成长档案整理助手。请根据旧档案和最新主题摘要,"
- "滚动更新一份结构化 JSON。只保留稳定模式和最近进展,避免堆叠流水账。"
- "输出字段:profileText, recurringTopics, commonEmotions, bodyPatterns, relationPatterns, "
- "homeworkDone, effectiveHomework, recentProgress。语气客观、温和,不诊断、不贴标签。\n\n"
- f"旧档案:\n{_limit(current, 5000)}\n\n最新主题摘要:\n{json.dumps(topic_summary_dict(summary), ensure_ascii=False, default=str)}"
+ "你是大本营千问千答的近期实修回顾助手。请依据最近30天内、最多10条主题摘要输出 JSON,"
+ "字段仅包含 reviewText 和 currentFocus。reviewText 用一小段中性文字回顾最近明确谈到的主题;"
+ "currentFocus 最多列出3项近期关注。不得继承或推断长期人格、情绪模式、身体模式、关系模式,"
+ "不得记录做过或有效的功课,不评价进步、变化、阶段或结果,不把一次表达固化为长期特征。"
+ "使用‘最近谈到/近期关注’而不是‘你总是/你已经形成’。证据不足时直接说明近期记录较少。\n\n"
+ f"近期主题摘要:\n{json.dumps(evidence, ensure_ascii=False, default=str)}"
)
-def _fallback_summary(topic: TopicSession, conversation: str, raw: str) -> dict[str, str]:
- source = raw.strip() if raw.strip() and not raw.strip().startswith("你是大本营") else conversation
+def _fallback_summary(topic: TopicSession, user_content: str, raw: str) -> dict[str, str]:
+ source = user_content or raw.strip()
user_lines = [line for line in source.splitlines() if line.startswith("用户:")]
- assistant_lines = [line for line in source.splitlines() if line.startswith("大本营答疑助手:")]
first_question = user_lines[0].removeprefix("用户:").strip() if user_lines else topic.core_question
- last_answer = assistant_lines[-1].removeprefix("大本营答疑助手:").strip() if assistant_lines else ""
+ last_user_text = user_lines[-1].removeprefix("用户:").strip() if user_lines else first_question
return {
- "summary": _limit(f"用户围绕“{first_question}”进行了提问,本主题的主要回答和沉淀为:{last_answer}", 1800),
- "mainEvents": _limit(first_question, 800),
- "emotions": _join_markers(source, ("害怕", "委屈", "愤怒", "抗拒", "焦虑", "担心", "难受")),
- "bodyFeelings": _join_markers(source, ("身体", "胸", "心口", "头", "胃", "麻", "胀", "紧", "堵")),
- "beliefs": _join_markers(source, ("必须", "应该", "不能", "总是", "一定", "不配")),
- "recommendedHomework": _join_markers(source, ("静心", "觉察", "功课", "练习", "释放", "内省")),
- "insights": _limit(last_answer, 1000),
- "nextObservation": "下次继续从当下最明显的身体感受、情绪和自动念头开始观察。",
+ "summary": _limit(f"本次主要谈到:{last_user_text}", 1800),
+ "currentFocus": _limit(first_question, 800),
+ "nextObservation": "可以继续留意这个问题在当下实际发生时,自己最直接的体验是什么。",
}
-def _fallback_profile(profile: UserGrowthProfile, summary: TopicSummary, raw: str) -> dict[str, str]:
- previous = profile.profile_text.strip()
- addition = raw.strip() if raw.strip() and not raw.strip().startswith("你是大本营") else summary.summary
- text = "\n\n".join(part for part in [previous, f"最近主题:{addition}"] if part)
+def _fallback_recent_review(summaries: list[TopicSummary], raw: str) -> dict[str, str]:
+ usable = [item for item in summaries if item.summary.strip()]
+ review_parts = [item.summary.strip() for item in usable[:3]]
+ focus_parts = [item.main_events.strip() for item in usable if item.main_events and item.main_events.strip()]
return {
- "profileText": _limit(text or "暂无成长档案。", 4500),
- "recurringTopics": summary.main_events or "",
- "commonEmotions": summary.emotions or "",
- "bodyPatterns": summary.body_feelings or "",
- "relationPatterns": summary.beliefs or "",
- "homeworkDone": summary.recommended_homework or "",
- "effectiveHomework": summary.recommended_homework or "",
- "recentProgress": summary.insights or summary.summary,
+ "reviewText": _limit(";".join(review_parts) or "近期沉淀记录较少。", 2600),
+ "currentFocus": _limit(";".join(dict.fromkeys(focus_parts[:3])), 900),
}
@@ -423,7 +452,7 @@ def _parse_summary_json(raw: str) -> dict[str, Any] | None:
return None
# 部分模型会把推理过程和最终 JSON 一起放进 answer,甚至在推理中先给出一份
# 草稿 JSON。先移除推理块,再从后向前选取可完整解析的对象,避免把思考过程
- # 或多个 JSON 拼接后写入用户成长档案。
+ # 或多个 JSON 拼接后写入近期实修回顾。
text = re.sub(r"<(?:think|analysis)>[\s\S]*?(?:think|analysis)>", "", text, flags=re.IGNORECASE).strip()
fenced = re.findall(r"```(?:json)?\s*([\s\S]*?)\s*```", text, flags=re.IGNORECASE)
candidates = [*fenced, text]
@@ -449,9 +478,13 @@ def _last_json_object(text: str) -> dict[str, Any] | None:
return parsed_objects[-1] if parsed_objects else None
-def _messages_text(messages: list[ChatMessage]) -> str:
+def _messages_text(messages: list[ChatMessage], *, roles: set[str] | None = None) -> str:
labels = {"user": "用户", "assistant": "大本营答疑助手"}
- return "\n".join(f"{labels.get(message.role, message.role)}:{message.content}" for message in messages)
+ return "\n".join(
+ f"{labels.get(message.role, message.role)}:{message.content}"
+ for message in messages
+ if roles is None or message.role in roles
+ )
def _field(data: dict[str, Any], key: str, default: str, max_len: int) -> str:
@@ -463,9 +496,33 @@ def _field(data: dict[str, Any], key: str, default: str, max_len: int) -> str:
return _limit(value.strip(), max_len)
-def _join_markers(text: str, markers: tuple[str, ...]) -> str:
- found = [marker for marker in markers if marker in text]
- return ";".join(found)
+def _json_int_list(raw: str | None) -> list[int]:
+ if not raw:
+ return []
+ try:
+ value = json.loads(raw)
+ except json.JSONDecodeError:
+ return []
+ if not isinstance(value, list):
+ return []
+ return [int(item) for item in value if isinstance(item, int) or (isinstance(item, str) and item.isdigit())]
+
+
+def _recent_review_summaries(db: Session, *, user_id: int) -> list[TopicSummary]:
+ cutoff = _now() - timedelta(days=RECENT_REVIEW_DAYS)
+ return list(
+ db.scalars(
+ select(TopicSummary)
+ .where(
+ TopicSummary.user_id == user_id,
+ TopicSummary.schema_version == RECENT_REVIEW_SCHEMA_VERSION,
+ TopicSummary.status.in_(("success", "fallback")),
+ TopicSummary.generated_at >= cutoff,
+ )
+ .order_by(TopicSummary.generated_at.desc(), TopicSummary.id.desc())
+ .limit(RECENT_REVIEW_TOPIC_LIMIT)
+ )
+ )
def _limit(text: str, max_len: int) -> str:
@@ -477,6 +534,7 @@ def _now() -> datetime:
def _reset_summary_job(summary: TopicSummary) -> None:
+ summary.schema_version = RECENT_REVIEW_SCHEMA_VERSION
summary.summary = ""
summary.main_events = None
summary.emotions = None
diff --git a/ai_knowledge_base_v2/apps/backend/app/services/help_card_service.py b/ai_knowledge_base_v2/apps/backend/app/services/help_card_service.py
index 56cf855..e380afd 100644
--- a/ai_knowledge_base_v2/apps/backend/app/services/help_card_service.py
+++ b/ai_knowledge_base_v2/apps/backend/app/services/help_card_service.py
@@ -87,7 +87,7 @@ def _latest_topic(db: Session, *, user: User, session: ChatSession) -> TopicSess
def _render_help_card(*, user: User, topic: TopicSession, summary: TopicSummary) -> str:
- data = topic_summary_dict(summary)
+ data = topic_summary_dict(summary) or {}
return (
"【给老师的求助卡】\n"
"说明:这是我根据本次 AI 对话整理出的求助信息,请老师帮我确认方向。"
@@ -99,14 +99,11 @@ def _render_help_card(*, user: User, topic: TopicSession, summary: TopicSummary)
f"{topic.core_question or '(请补充)'}\n\n"
"2. AI 已经帮我梳理出的重点\n"
f"{data.get('summary') or '(暂无摘要)'}\n\n"
- "3. 我现在最明显的情绪 / 身体感受\n"
- f"情绪:{data.get('emotions') or '(请补充)'}\n"
- f"身体感受:{data.get('bodyFeelings') or '(请补充)'}\n\n"
- "4. 我已经尝试过或被建议的功课\n"
- f"{data.get('recommendedHomework') or '(请补充)'}\n\n"
- "5. 我仍然卡住的地方\n"
- f"{data.get('beliefs') or data.get('nextObservation') or '(请补充)'}\n\n"
- "6. 我想请老师确认的问题\n"
+ "3. 我这次主要关注的内容\n"
+ f"{data.get('currentFocus') or topic.core_question or '(请补充)'}\n\n"
+ "4. 我还想继续留意的地方\n"
+ f"{data.get('nextObservation') or '(请补充)'}\n\n"
+ "5. 我想请老师确认的问题\n"
"(请把最想确认的一两个问题写在这里)\n\n"
"备注:这张卡片不会自动发送给老师,也不代表已经转人工处理。"
)
@@ -120,4 +117,3 @@ def _format_time(value: datetime | None) -> str:
def _now() -> datetime:
return datetime.now(UTC).replace(tzinfo=None)
-
diff --git a/ai_knowledge_base_v2/apps/backend/app/services/periodic_report_service.py b/ai_knowledge_base_v2/apps/backend/app/services/periodic_report_service.py
index 253a749..c2db73b 100644
--- a/ai_knowledge_base_v2/apps/backend/app/services/periodic_report_service.py
+++ b/ai_knowledge_base_v2/apps/backend/app/services/periodic_report_service.py
@@ -10,17 +10,19 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from app.core.config import get_settings
-from app.models.growth import PeriodicReport, TopicSummary, UserGrowthProfile
+from app.models.growth import PeriodicReport, TopicSummary
from app.models.user import User
+from app.services.reasoning_policy_service import ReasoningPolicyService
from app.services.tracked_generation_service import TrackedGenerationService
ReportType = Literal["weekly", "monthly", "stage"]
REPORT_TYPE_LABELS = {
"weekly": "每周实修小结",
- "monthly": "每月成长报告",
- "stage": "阶段成长总结",
+ "monthly": "每月实修回顾",
+ "stage": "阶段实修回顾",
}
+REPORT_SCHEMA_VERSION = 2
class PeriodicReportService:
@@ -46,7 +48,10 @@ class PeriodicReportService:
limit: int = 20,
statuses: Iterable[str] | None = None,
) -> list[PeriodicReport]:
- query = select(PeriodicReport).where(PeriodicReport.user_id == user_id)
+ query = select(PeriodicReport).where(
+ PeriodicReport.user_id == user_id,
+ PeriodicReport.schema_version == REPORT_SCHEMA_VERSION,
+ )
if statuses is not None:
query = query.where(PeriodicReport.status.in_(tuple(statuses)))
return list(
@@ -104,6 +109,7 @@ class PeriodicReportService:
if report.status == "running":
return report
report.title = _report_title(report_type, period_start, period_end)
+ report.schema_version = REPORT_SCHEMA_VERSION
report.status = "pending"
report.error_message = None
report.generated_by = generated_by
@@ -159,9 +165,9 @@ class PeriodicReportService:
raise ValueError("不支持的报告类型")
period_start = report.period_start
period_end = report.period_end
+ report.schema_version = REPORT_SCHEMA_VERSION
report.title = _report_title(report_type, period_start, period_end)
summaries = _period_summaries(db, user_id=user.id, period_start=period_start, period_end=period_end)
- profile = db.scalar(select(UserGrowthProfile).where(UserGrowthProfile.user_id == user.id))
topic_ids = sorted({int(item.topic_session_id) for item in summaries})
summary_ids = [int(item.id) for item in summaries]
report.source_topic_ids = json.dumps(topic_ids, ensure_ascii=False)
@@ -176,14 +182,20 @@ class PeriodicReportService:
return report
try:
- prompt = _report_prompt(user=user, report_type=report_type, period_start=period_start, period_end=period_end, summaries=summaries, profile=profile)
+ prompt = _report_prompt(
+ user=user,
+ report_type=report_type,
+ period_start=period_start,
+ period_end=period_end,
+ summaries=summaries,
+ )
completion = TrackedGenerationService.generate(
db,
prompt=prompt,
scenario="report",
user_id=user.id,
)
- report.content = completion.answer.strip() or _fallback_report(summaries)
+ report.content = ReasoningPolicyService.strip_reasoning(completion.answer).strip() or _fallback_report(summaries)
report.model_name = completion.model_name
report.status = "success"
report.error_message = None
@@ -199,6 +211,7 @@ def periodic_report_dict(report: PeriodicReport) -> dict:
return {
"id": report.id,
"userId": report.user_id,
+ "schemaVersion": report.schema_version,
"reportType": report.report_type,
"reportTypeLabel": REPORT_TYPE_LABELS.get(report.report_type, report.report_type),
"periodStart": _local_datetime(report.period_start),
@@ -289,6 +302,7 @@ def _new_report(
) -> PeriodicReport:
return PeriodicReport(
user_id=user.id,
+ schema_version=REPORT_SCHEMA_VERSION,
report_type=report_type,
period_start=period_start,
period_end=period_end,
@@ -332,6 +346,7 @@ def _period_summaries(db: Session, *, user_id: int, period_start: datetime, peri
select(TopicSummary)
.where(
TopicSummary.user_id == user_id,
+ TopicSummary.schema_version == REPORT_SCHEMA_VERSION,
TopicSummary.status == "success",
TopicSummary.generated_at >= period_start,
TopicSummary.generated_at < period_end,
@@ -349,7 +364,6 @@ def _report_prompt(
period_start: datetime,
period_end: datetime,
summaries: list[TopicSummary],
- profile: UserGrowthProfile | None,
) -> str:
local_start = _local_datetime(period_start)
local_end = _local_datetime(period_end)
@@ -357,11 +371,7 @@ def _report_prompt(
(
f"主题摘要 {index}\n"
f"摘要:{item.summary}\n"
- f"主要事件:{item.main_events or '无'}\n"
- f"情绪:{item.emotions or '无'}\n"
- f"身体感受:{item.body_feelings or '无'}\n"
- f"推荐功课:{item.recommended_homework or '无'}\n"
- f"看见/变化:{item.insights or '无'}\n"
+ f"近期关注:{item.main_events or '无'}\n"
f"下一步观察:{item.next_observation or '无'}"
)
for index, item in enumerate(summaries, start=1)
@@ -369,23 +379,21 @@ def _report_prompt(
return (
f"请为学员“{user.name or user.nickname or user.phone}”生成一份{REPORT_TYPE_LABELS[report_type]}。\n"
f"周期:{local_start:%Y-%m-%d %H:%M} 至 {local_end:%Y-%m-%d %H:%M}。\n\n"
- "产品定位:这是大本营千问千答的实修陪伴报告,不写成医疗诊断、心理咨询结论或营销文。\n"
- "表达方向:回到当下、回到自身、觉察情绪和身体感受,如实释放;建议适度,不要给过多术层面的复杂方案。\n"
- "请使用 Markdown 输出,结构包含:本周期主要议题、做过或被建议的功课、反复出现的情绪/身体模式、已有变化、下一步观察方向、可以带给老师确认的问题。\n"
+ "产品定位:这是帮助学员回到当下的近期实修回顾,不是成长成绩单、医疗诊断或心理咨询结论。\n"
+ "只依据本周期主题摘要,使用 Markdown 输出:本周期谈到的主题、近期关注、可以继续留意的问题。\n"
+ "不要统计或归纳情绪、身体、关系模式,不列做过或有效的功课,不评价进步、变化、阶段和结果,"
+ "不设置下一阶段目标。使用‘最近谈到/可以继续留意’,避免‘你总是/你已经形成’。\n"
"如果证据不足,请明确说“本周期沉淀记录较少”,不要编造。\n\n"
- f"长期成长档案:\n{profile.profile_text if profile else '暂无'}\n\n"
f"本周期主题摘要:\n{summary_text}"
)
def _fallback_report(summaries: list[TopicSummary]) -> str:
- lines = ["## 本周期实修小结", "", "模型生成失败时,系统先根据已沉淀主题摘要生成基础版本:"]
+ lines = ["## 本周期实修回顾", "", "以下内容根据本周期已沉淀的主题整理:"]
for item in summaries[:20]:
lines.extend(["", f"- {item.summary}"])
- if item.recommended_homework:
- lines.append(f" - 建议功课:{item.recommended_homework}")
if item.next_observation:
- lines.append(f" - 下一步观察:{item.next_observation}")
+ lines.append(f" - 可以继续留意:{item.next_observation}")
return "\n".join(lines)
diff --git a/ai_knowledge_base_v2/apps/backend/app/services/share_draft_service.py b/ai_knowledge_base_v2/apps/backend/app/services/share_draft_service.py
index 9df49a9..3c481d7 100644
--- a/ai_knowledge_base_v2/apps/backend/app/services/share_draft_service.py
+++ b/ai_knowledge_base_v2/apps/backend/app/services/share_draft_service.py
@@ -86,26 +86,21 @@ def _latest_topic(db: Session, *, user: User, session: ChatSession) -> TopicSess
def _render_share_draft(*, topic: TopicSession, summary: TopicSummary) -> str:
- data = topic_summary_dict(summary)
+ data = topic_summary_dict(summary) or {}
return (
"【实修分享稿草稿】\n"
"说明:这是根据我本次对话整理出的分享草稿,系统不会自动发送到任何群,"
"我会按真实情况删改后再决定是否发到班级群。\n\n"
- "大家好,我想分享一下这次实修里看到的一点东西。\n\n"
- "1. 我这次观察到的议题\n"
+ "大家好,我想分享一下这次实修中正在关注的内容。\n\n"
+ "1. 我这次谈到的主题\n"
f"{topic.core_question or topic.title}\n\n"
- "2. 我看见了什么\n"
- f"{data.get('summary') or '(请用自己的话补充真实看见)'}\n\n"
- "3. 我感受到的情绪和身体反应\n"
- f"情绪:{data.get('emotions') or '(请补充)'}\n"
- f"身体:{data.get('bodyFeelings') or '(请补充)'}\n\n"
- "4. 我做了什么功课 / 准备继续做什么\n"
- f"{data.get('recommendedHomework') or '(请补充)'}\n\n"
- "5. 当下的一点变化\n"
- f"{data.get('insights') or '(请补充真实变化,不需要夸大)'}\n\n"
- "6. 我还在继续观察的方向\n"
+ "2. 这次对话的简要回顾\n"
+ f"{data.get('summary') or '(请用自己的话补充)'}\n\n"
+ "3. 我近期正在关注什么\n"
+ f"{data.get('currentFocus') or '(请补充)'}\n\n"
+ "4. 我还想继续留意的方向\n"
f"{data.get('nextObservation') or '(请补充)'}\n\n"
- "备注:这只是我的阶段性观察,不代表已经彻底解决,也不是建议别人照搬。"
+ "备注:这只是我当下的一次回顾,不代表结论,也不是建议别人照搬。"
)
diff --git a/ai_knowledge_base_v2/apps/backend/app/services/topic_session_service.py b/ai_knowledge_base_v2/apps/backend/app/services/topic_session_service.py
index 1a2c59b..f8186c8 100644
--- a/ai_knowledge_base_v2/apps/backend/app/services/topic_session_service.py
+++ b/ai_knowledge_base_v2/apps/backend/app/services/topic_session_service.py
@@ -127,9 +127,7 @@ class TopicSessionService:
f"核心问题:{_limit(topic.core_question, 1200)}",
f"主题状态:{topic.status}",
]
- if topic.recommended_homework:
- lines.append(f"已记录的建议功课:{_limit(topic.recommended_homework, 1000)}")
- if summary is not None and summary.status in {"success", "fallback"}:
+ if summary is not None and summary.schema_version >= 2 and summary.status in {"success", "fallback"}:
lines.extend(
[
"\n[所选主题摘要]",
@@ -137,12 +135,8 @@ class TopicSessionService:
]
)
details = [
- ("主要事件", summary.main_events),
- ("情绪", summary.emotions),
- ("身体感受", summary.body_feelings),
- ("信念", summary.beliefs),
- ("建议功课", summary.recommended_homework),
- ("下一步观察", summary.next_observation),
+ ("本次关注", summary.main_events),
+ ("可以继续留意", summary.next_observation),
]
lines.extend(f"{label}:{_limit(value, 800)}" for label, value in details if value)
return "\n".join(lines), True
diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_agent_debug_user_context.py b/ai_knowledge_base_v2/apps/backend/tests/test_agent_debug_user_context.py
index 0a16c4b..fe52b11 100644
--- a/ai_knowledge_base_v2/apps/backend/tests/test_agent_debug_user_context.py
+++ b/ai_knowledge_base_v2/apps/backend/tests/test_agent_debug_user_context.py
@@ -37,7 +37,15 @@ def test_agent_debug_can_simulate_user_growth_profile_context():
status=1,
)
)
- db.add(UserGrowthProfile(user_id=1, profile_text="用户在表达障碍主题上反复出现身体紧绷。"))
+ db.add(
+ UserGrowthProfile(
+ user_id=1,
+ schema_version=2,
+ recent_review="最近谈到表达时不知道怎么回到当下。",
+ current_focus="表达时先看见当下",
+ profile_text="旧版画像不得注入",
+ )
+ )
db.commit()
result = asyncio.run(
@@ -53,10 +61,11 @@ def test_agent_debug_can_simulate_user_growth_profile_context():
)
rendered = "\n".join(item["content"] for item in result.messages)
- assert "[长期成长档案]" in rendered
- assert "表达障碍" in rendered
+ assert "[近期实修回顾]" in rendered
+ assert "表达时先看见当下" in rendered
+ assert "旧版画像不得注入" not in rendered
assert result.tool_trace[0]["tool"] == "load_debug_user_context"
- assert result.tool_trace[0]["response"]["growthProfileUsed"] is True
+ assert result.tool_trace[0]["response"]["recentPracticeReviewUsed"] is True
assert "[当前产品权益]" in rendered
@@ -88,7 +97,13 @@ def test_agent_debug_loads_selected_topic_history_summary_and_permissions():
allow_share_draft=0,
status=1,
),
- UserGrowthProfile(user_id=1, profile_text="用户在表达时容易身体紧绷。"),
+ UserGrowthProfile(
+ user_id=1,
+ schema_version=2,
+ recent_review="最近谈到面对领导时不敢表达。",
+ current_focus="面对领导时的当下表达",
+ profile_text="",
+ ),
ChatMessage(
id=100,
session_id=20,
@@ -109,6 +124,7 @@ def test_agent_debug_loads_selected_topic_history_summary_and_permissions():
topic_session_id=30,
user_id=1,
summary="学员正在观察面对权威时的紧绷。",
+ main_events="面对领导时如何表达",
emotions="害怕",
body_feelings="心口紧",
status="success",
@@ -134,6 +150,9 @@ def test_agent_debug_loads_selected_topic_history_summary_and_permissions():
rendered = "\n".join(item["content"] for item in result.messages)
assert "[当前对话主题]" in rendered
assert "[所选主题摘要]" in rendered
+ assert "本次关注:面对领导时如何表达" in rendered
+ assert "情绪:害怕" not in rendered
+ assert "身体感受:心口紧" not in rendered
assert "我又不敢说话了" in rendered
assert "我想继续刚才的主题" in rendered
assert "老师求助卡:可由学员主动生成" in rendered
diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_growth_profiles.py b/ai_knowledge_base_v2/apps/backend/tests/test_growth_profiles.py
index 9acabfe..9fc438d 100644
--- a/ai_knowledge_base_v2/apps/backend/tests/test_growth_profiles.py
+++ b/ai_knowledge_base_v2/apps/backend/tests/test_growth_profiles.py
@@ -72,7 +72,15 @@ def test_finish_topic_enqueues_summary_and_worker_updates_growth_profile_for_ena
assert completed.status == "success"
assert completed.summary
profile = db.query(UserGrowthProfile).filter_by(user_id=1).one()
- assert "最近主题" in profile.profile_text
+ assert profile.schema_version == 2
+ assert "本次主要谈到" in (profile.recent_review or "")
+ assert profile.current_focus == "我做阴影人格会抗拒,身体发紧"
+ assert profile.profile_text == ""
+ assert profile.common_emotions is None
+ assert profile.body_patterns is None
+ assert profile.homework_done is None
+ assert profile.effective_homework is None
+ assert profile.recent_progress is None
assert db.query(UserGrowthProfile).filter_by(user_id=1).count() == 1
assert db.query(GrowthProfileRevision).filter_by(user_id=1).count() == 1
@@ -167,11 +175,21 @@ def test_stale_topic_settlement_is_recovered_after_restart():
assert "自动恢复" in (summary.error_message or "")
-def test_prompt_can_include_growth_profile_context_without_replacing_knowledge_context():
+def test_prompt_includes_only_v2_recent_review_without_replacing_knowledge_context():
with _db() as db:
user = User(id=1, phone="13800000001", name="学员", daily_chat_limit=100, daily_chat_used=0)
db.add(user)
- db.add(UserGrowthProfile(user_id=1, profile_text="用户最近反复在表达障碍和身体紧绷之间观察。"))
+ db.add(
+ UserGrowthProfile(
+ user_id=1,
+ schema_version=2,
+ recent_review="最近谈到面对领导时不敢表达。",
+ current_focus="面对领导时如何回到当下",
+ profile_text="旧画像不应进入提示词",
+ common_emotions="紧张",
+ body_patterns="身体紧绷",
+ )
+ )
db.commit()
growth_context = GrowthProfileService.prompt_context(db, user)
@@ -183,24 +201,45 @@ def test_prompt_can_include_growth_profile_context_without_replacing_knowledge_c
)
content = "\n".join(item["content"] for item in messages)
- assert "[长期成长档案]" in content
- assert "表达障碍" in content
+ assert "[近期实修回顾]" in content
+ assert "面对领导时如何回到当下" in content
+ assert "旧画像不应进入提示词" not in content
+ assert "身体紧绷" not in content
+ assert "常见情绪" not in content
assert "[本轮可靠知识上下文]" in content
+def test_legacy_growth_profile_is_not_injected_before_v2_rebuild():
+ with _db() as db:
+ user = User(id=1, phone="13800000001", name="学员", daily_chat_limit=100, daily_chat_used=0)
+ db.add_all(
+ [
+ user,
+ UserGrowthProfile(
+ user_id=1,
+ schema_version=1,
+ profile_text="旧版长期人格画像",
+ common_emotions="焦虑",
+ ),
+ ]
+ )
+ db.commit()
+
+ assert GrowthProfileService.prompt_context(db, user) is None
+
+
def test_summary_json_parser_ignores_model_reasoning_and_uses_final_json():
raw = """
先分析一下,并给出一个未完成草稿:
```json
-{"profileText": "不应采用的草稿"}
+{"reviewText": "不应采用的草稿"}
```
```json
{
- "profileText": "只保留最终成长档案",
- "commonEmotions": ["紧张"],
- "recentProgress": ["开始观察身体感受"]
+ "reviewText": "只保留最终近期回顾",
+ "currentFocus": ["当下正在谈到的问题"]
}
```
"""
@@ -208,5 +247,5 @@ def test_summary_json_parser_ignores_model_reasoning_and_uses_final_json():
parsed = _parse_summary_json(raw)
assert parsed is not None
- assert parsed["profileText"] == "只保留最终成长档案"
+ assert parsed["reviewText"] == "只保留最终近期回顾"
assert "think" not in str(parsed).lower()
diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_help_cards.py b/ai_knowledge_base_v2/apps/backend/tests/test_help_cards.py
index 14f6265..e87c6ae 100644
--- a/ai_knowledge_base_v2/apps/backend/tests/test_help_cards.py
+++ b/ai_knowledge_base_v2/apps/backend/tests/test_help_cards.py
@@ -54,6 +54,8 @@ def test_generate_help_card_from_topic_summary_and_mark_copied():
assert "给老师的求助卡" in card.content
assert "不会自动发送给老师" in card.content
assert "阴影人格练习步骤是否正确" in card.content
+ assert "情绪 / 身体感受" not in card.content
+ assert "已经尝试过或被建议的功课" not in card.content
assert db.get(TopicSession, 1).help_card_generated == 1
assert db.query(TeacherHelpCard).count() == 1
@@ -61,4 +63,3 @@ def test_generate_help_card_from_topic_summary_and_mark_copied():
assert copied.copied == 1
assert copied.copied_at is not None
-
diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_periodic_reports.py b/ai_knowledge_base_v2/apps/backend/tests/test_periodic_reports.py
index 0c5e8bf..f9eca04 100644
--- a/ai_knowledge_base_v2/apps/backend/tests/test_periodic_reports.py
+++ b/ai_knowledge_base_v2/apps/backend/tests/test_periodic_reports.py
@@ -14,6 +14,7 @@ from app.models.growth import PeriodicReport, TopicSummary, UserGrowthProfile
from app.models.user import User
from app.services.periodic_report_service import PeriodicReportService, periodic_report_dict
from app.services.periodic_report_worker import PeriodicReportWorker, scheduled_period
+from app.services.model_service import ModelCompletion
from app.services.tracked_generation_service import TrackedGenerationService
@@ -51,7 +52,11 @@ def test_generate_periodic_report_from_topic_summaries():
report = PeriodicReportService.generate_for_user(db, user=user, report_type="weekly", period_start=now - timedelta(days=7), period_end=now)
assert report.status == "success"
+ assert report.schema_version == 2
assert report.content
+ assert "长期成长档案" not in report.content
+ assert "推荐功课:" not in report.content
+ assert "情绪:害怕" not in report.content
data = periodic_report_dict(report)
assert data["sourceSummaryIds"] == [1]
assert data["sourceTopicIds"] == [1]
@@ -71,6 +76,68 @@ def test_generate_empty_periodic_report_when_no_summaries():
assert periodic_report_dict(report)["sourceSummaryIds"] == []
+def test_periodic_report_strips_model_reasoning_before_saving(monkeypatch):
+ with _db() as db:
+ now = _now()
+ user = User(id=1, phone="13800000001", name="学员", daily_chat_limit=100, daily_chat_used=0)
+ session = ChatSession(id=1, user_id=1, title="当下", message_count=1, last_message_at=now, is_deleted=0)
+ topic = TopicSession(id=1, user_id=1, chat_session_id=1, title="当下", core_question="我想先看当下", status="completed")
+ summary = TopicSummary(
+ id=1,
+ user_id=1,
+ topic_session_id=1,
+ summary="最近谈到想先看当下。",
+ generated_at=now - timedelta(days=1),
+ )
+ db.add_all([user, session, topic, summary])
+ db.commit()
+ monkeypatch.setattr(
+ TrackedGenerationService,
+ "generate",
+ staticmethod(
+ lambda *_args, **_kwargs: ModelCompletion(
+ answer="
内部分析与旧画像标签\n## 本周期谈到的主题\n\n最近谈到想先看当下。",
+ model_id=1,
+ model_name="test-model",
+ input_token=1,
+ output_token=1,
+ )
+ ),
+ )
+
+ report = PeriodicReportService.generate_for_user(
+ db,
+ user=user,
+ report_type="weekly",
+ period_start=now - timedelta(days=7),
+ period_end=now,
+ )
+
+ assert report.content.startswith("## 本周期谈到的主题")
+ assert "内部分析" not in report.content
+ assert "
" not in report.content
+
+
+def test_legacy_periodic_reports_are_hidden_until_regenerated():
+ with _db() as db:
+ now = _now()
+ user = User(id=1, phone="13800000001", name="学员", daily_chat_limit=100, daily_chat_used=0)
+ legacy = PeriodicReport(
+ user_id=1,
+ schema_version=1,
+ report_type="weekly",
+ period_start=now - timedelta(days=7),
+ period_end=now,
+ title="旧版成长报告",
+ content="常见情绪、身体模式、有效功课和近期变化",
+ status="success",
+ )
+ db.add_all([user, legacy])
+ db.commit()
+
+ assert PeriodicReportService.list_user_reports(db, user_id=1) == []
+
+
def test_async_report_job_is_durable_and_idempotent():
with _db() as db:
now = _now()
diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_share_drafts.py b/ai_knowledge_base_v2/apps/backend/tests/test_share_drafts.py
index 4b63993..740efd3 100644
--- a/ai_knowledge_base_v2/apps/backend/tests/test_share_drafts.py
+++ b/ai_knowledge_base_v2/apps/backend/tests/test_share_drafts.py
@@ -53,7 +53,10 @@ def test_generate_share_draft_from_topic_summary_and_mark_copied():
assert "实修分享稿草稿" in draft.content
assert "系统不会自动发送到任何群" in draft.content
- assert "不代表已经彻底解决" in draft.content
+ assert "不代表结论" in draft.content
+ assert "情绪和身体反应" not in draft.content
+ assert "做了什么功课" not in draft.content
+ assert "当下的一点变化" not in draft.content
assert "我看见自己不敢表达" in draft.content
assert db.get(TopicSession, 1).share_draft_generated == 1
assert db.query(ShareDraft).count() == 1
diff --git a/ai_knowledge_base_v2/apps/user-client/src/App.vue b/ai_knowledge_base_v2/apps/user-client/src/App.vue
index 6ccf094..0909185 100644
--- a/ai_knowledge_base_v2/apps/user-client/src/App.vue
+++ b/ai_knowledge_base_v2/apps/user-client/src/App.vue
@@ -9,7 +9,7 @@ import MessageList, { type DisplayMessage } from "./components/MessageList.vue";
import SessionDrawer from "./components/SessionDrawer.vue";
import SessionQuota from "./components/SessionQuota.vue";
import { ApiError, api, clearToken, getToken, streamChat } from "./services/api";
-import type { ChatMessage as ApiMessage, ChatSession, GrowthProfileResult, PeriodicReport, ShareDraft, TeacherHelpCard, UserProfile } from "./types/api";
+import type { ChatMessage as ApiMessage, ChatSession, PeriodicReport, PracticeReviewResult, ShareDraft, TeacherHelpCard, UserProfile } from "./types/api";
const user = ref(null);
const sessions = ref([]);
@@ -25,7 +25,7 @@ const logoutDialogOpen = ref(false);
const profileDialogOpen = ref(false);
const helpCardDialogOpen = ref(false);
const shareDraftDialogOpen = ref(false);
-const growthProfile = ref(null);
+const practiceReview = ref(null);
const helpCard = ref(null);
const helpCardContent = ref("");
const shareDraft = ref(null);
@@ -218,7 +218,7 @@ async function finishCurrentTopic() {
try {
const result = await api.finishTopic(activeSessionId.value);
if (result.settlementStatus === "success" || result.settlementStatus === "fallback") {
- showToast(result.growthProfileEnabled ? "本主题已沉淀,并更新了你的成长档案" : "本主题已沉淀");
+ showToast(result.growthProfileEnabled ? "本主题已沉淀,并更新了近期实修回顾" : "本主题已沉淀");
} else {
showToast("主题已结束,实修记录正在后台沉淀,可以继续使用其他功能");
void watchTopicSettlement(result.summary.id);
@@ -240,7 +240,7 @@ async function watchTopicSettlement(summaryId: number) {
try {
const result = await api.topicSettlement(summaryId);
if (result.settlementStatus === "success" || result.settlementStatus === "fallback") {
- showToast(result.growthProfileEnabled ? "实修记录已沉淀,并更新了成长档案" : "实修记录已沉淀完成");
+ showToast(result.growthProfileEnabled ? "实修记录已沉淀,并更新了近期实修回顾" : "实修记录已沉淀完成");
await refreshProfile();
return;
}
@@ -330,22 +330,22 @@ async function copyShareDraft() {
}
}
-async function openGrowthProfile() {
+async function openPracticeReview() {
profileDialogOpen.value = true;
profileLoading.value = true;
try {
const [profileResult, helpCards, shareDrafts, reports] = await Promise.all([
- api.growthProfile(),
+ api.practiceReview(),
api.helpCards(10),
api.shareDrafts(10),
api.periodicReports(10),
]);
- growthProfile.value = profileResult;
+ practiceReview.value = profileResult;
helpCardHistory.value = helpCards;
shareDraftHistory.value = shareDrafts;
reportHistory.value = reports;
} catch (error) {
- handleError(error, "成长档案加载失败");
+ handleError(error, "近期实修回顾加载失败");
} finally {
profileLoading.value = false;
}
@@ -484,7 +484,7 @@ async function copyText(text: string) {
:generating-help-card="generatingHelpCard"
:generating-share-draft="generatingShareDraft"
@finish-topic="finishCurrentTopic"
- @open-profile="openGrowthProfile"
+ @open-profile="openPracticeReview"
@generate-help-card="generateHelpCard"
@generate-share-draft="generateShareDraft"
/>
@@ -512,23 +512,19 @@ async function copyText(text: string) {
{{ toastText }}
-
+
- 正在加载成长档案...
-
- {{ growthProfile.profile.profileText }}
-
-
反复议题{{ growthProfile.profile.recurringTopics }}
-
常见情绪{{ growthProfile.profile.commonEmotions }}
-
身体感受{{ growthProfile.profile.bodyPatterns }}
-
做过的功课{{ growthProfile.profile.homeworkDone }}
-
最近进展{{ growthProfile.profile.recentProgress }}
+
正在加载近期回顾...
+
+ {{ practiceReview.profile.reviewText }}
+
+
近期关注{{ practiceReview.profile.currentFocus }}
-
还没有成长档案。你可以在完成一次主题对话后点击“沉淀本主题”。
-
+
还没有近期回顾。你可以在完成一次主题对话后点击“沉淀本主题”。
+
最近主题沉淀
-
+
{{ item.summary }}
沉淀暂时失败,对话内容仍已保留。
@@ -536,7 +532,7 @@ async function copyText(text: string) {
-
周期实修报告
+
周期实修回顾
{{ item.title }}
diff --git a/ai_knowledge_base_v2/apps/user-client/src/components/SessionQuota.vue b/ai_knowledge_base_v2/apps/user-client/src/components/SessionQuota.vue
index b0145ac..583e381 100644
--- a/ai_knowledge_base_v2/apps/user-client/src/components/SessionQuota.vue
+++ b/ai_knowledge_base_v2/apps/user-client/src/components/SessionQuota.vue
@@ -52,7 +52,7 @@ const guidanceText = computed(() => {
class="quota-link"
@click="$emit('openProfile')"
>
- 我的档案
+ 近期回顾