feat: align agent preview user context

This commit is contained in:
2026-07-31 15:48:40 +08:00
parent 3e60a6da42
commit 79094d0832
6 changed files with 168 additions and 3 deletions

View File

@@ -3,7 +3,7 @@ import { ElMessage, ElMessageBox } from "element-plus";
import { computed, nextTick, onMounted, reactive, ref, watch } from "vue"; import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
import { api, streamDebugAgent } from "../services/api"; import { api, streamDebugAgent } from "../services/api";
import type { AgentRuntimeConfig, KnowledgeItem, ModelItem, PromptDetail, PromptHistoryItem } from "../types/api"; import type { AdminUser, AgentRuntimeConfig, KnowledgeItem, ModelItem, PromptDetail, PromptHistoryItem } from "../types/api";
import AgentGenerationParameters from "./AgentGenerationParameters.vue"; import AgentGenerationParameters from "./AgentGenerationParameters.vue";
import AgentResponseDepthControl from "./AgentResponseDepthControl.vue"; import AgentResponseDepthControl from "./AgentResponseDepthControl.vue";
import AdminPagination from "./AdminPagination.vue"; import AdminPagination from "./AdminPagination.vue";
@@ -25,6 +25,8 @@ const promptContent = ref("");
const savedPromptContent = ref(""); const savedPromptContent = ref("");
const models = ref<ModelItem[]>([]); const models = ref<ModelItem[]>([]);
const knowledge = ref<KnowledgeItem[]>([]); const knowledge = ref<KnowledgeItem[]>([]);
const debugUsers = ref<AdminUser[]>([]);
const debugUserLoading = ref(false);
const history = ref<PromptHistoryItem[]>([]); const history = ref<PromptHistoryItem[]>([]);
const historyPager = reactive({ page: 1, pageSize: 10, total: 0 }); const historyPager = reactive({ page: 1, pageSize: 10, total: 0 });
const historyDetailOpen = ref(false); const historyDetailOpen = ref(false);
@@ -43,6 +45,7 @@ const agentPreviewMessages = ref<{
const agentForm = reactive({ const agentForm = reactive({
modelId: undefined as number | undefined, modelId: undefined as number | undefined,
userId: undefined as number | undefined,
knowledgeIds: [] as number[], knowledgeIds: [] as number[],
temperature: 0.2 as number | null, temperature: 0.2 as number | null,
topP: null as number | null, topP: null as number | null,
@@ -80,6 +83,11 @@ const selectedKnowledgeSummary = computed(() => {
} }
return `${agentForm.knowledgeIds.length} 个知识库`; return `${agentForm.knowledgeIds.length} 个知识库`;
}); });
const selectedDebugUserLabel = computed(() => {
if (!agentForm.userId) return "未模拟学员";
const user = debugUsers.value.find((item) => item.id === agentForm.userId);
return user ? `${user.name || user.nickname || user.phone} · #${user.id}` : `学员 #${agentForm.userId}`;
});
onMounted(load); onMounted(load);
@@ -103,6 +111,7 @@ async function load() {
.filter((item) => item.status === 1 && item.lifecycleStatus === "active") .filter((item) => item.status === 1 && item.lifecycleStatus === "active")
.map((item) => item.id); .map((item) => item.id);
if (props.previewKnowledgeId) applyPreviewKnowledge(props.previewKnowledgeId); if (props.previewKnowledgeId) applyPreviewKnowledge(props.previewKnowledgeId);
void searchDebugUsers("");
await loadHistory(); await loadHistory();
} catch (error) { } catch (error) {
ElMessage.error(errorMessage(error, "Agent 配置加载失败")); ElMessage.error(errorMessage(error, "Agent 配置加载失败"));
@@ -111,6 +120,18 @@ async function load() {
} }
} }
async function searchDebugUsers(keyword: string) {
debugUserLoading.value = true;
try {
const result = await api.users({ keyword, page: 1, pageSize: 20 });
debugUsers.value = result.items;
} catch {
debugUsers.value = [];
} finally {
debugUserLoading.value = false;
}
}
function applyRuntimeConfig(value: AgentRuntimeConfig) { function applyRuntimeConfig(value: AgentRuntimeConfig) {
runtimeConfig.value = value; runtimeConfig.value = value;
Object.assign(runtimeForm, { Object.assign(runtimeForm, {
@@ -299,6 +320,7 @@ async function debugAgent() {
{ {
promptContent: promptContent.value, promptContent: promptContent.value,
modelId: agentForm.modelId, modelId: agentForm.modelId,
userId: agentForm.userId || null,
knowledgeIds: agentForm.knowledgeIds, knowledgeIds: agentForm.knowledgeIds,
question, question,
history: conversationHistory, history: conversationHistory,
@@ -473,6 +495,25 @@ function errorMessage(error: unknown, fallback: string) {
<el-option v-for="model in models" :key="model.id" :label="model.displayName || model.modelName" :value="model.id" /> <el-option v-for="model in models" :key="model.id" :label="model.displayName || model.modelName" :value="model.id" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="模拟学员(可选)">
<el-select
v-model="agentForm.userId"
clearable
filterable
remote
:remote-method="searchDebugUsers"
:loading="debugUserLoading"
placeholder="不选择则不注入学员权益和成长档案"
>
<el-option
v-for="user in debugUsers"
:key="user.id"
:label="`${user.name || user.nickname || user.phone} · ${user.phone} · #${user.id}`"
:value="user.id"
/>
</el-select>
<div class="agent-form-help">用于让后台预览加载该学员的权益主题额度和长期成长档案不会消耗额度也不会影响正式会话</div>
</el-form-item>
<el-form-item label="调试知识库"> <el-form-item label="调试知识库">
<el-select v-model="agentForm.knowledgeIds" class="debug-knowledge-select" multiple filterable collapse-tags collapse-tags-tooltip :max-collapse-tags="2" popper-class="debug-knowledge-popper" placeholder="默认使用全部已开放知识库"> <el-select v-model="agentForm.knowledgeIds" class="debug-knowledge-select" multiple filterable collapse-tags collapse-tags-tooltip :max-collapse-tags="2" popper-class="debug-knowledge-popper" placeholder="默认使用全部已开放知识库">
<el-option v-for="item in knowledge" :key="item.id" :label="`${item.name}${item.status === 0 ? ' · 已关闭(仅本次预览)' : ''}`" :value="item.id" /> <el-option v-for="item in knowledge" :key="item.id" :label="`${item.name}${item.status === 0 ? ' · 已关闭(仅本次预览)' : ''}`" :value="item.id" />
@@ -546,7 +587,7 @@ function errorMessage(error: unknown, fallback: string) {
<aside class="agent-preview-panel"> <aside class="agent-preview-panel">
<div class="agent-preview-head"> <div class="agent-preview-head">
<div><h3>调试预览</h3><small>{{ selectedKnowledgeSummary }}</small></div> <div><h3>调试预览</h3><small>{{ selectedKnowledgeSummary }} · {{ selectedDebugUserLabel }}</small></div>
<div class="agent-preview-head-actions"><span>{{ selectedModelName }}</span><el-button link :disabled="agentDebugging" @click="clearAgentPreview">清空</el-button></div> <div class="agent-preview-head-actions"><span>{{ selectedModelName }}</span><el-button link :disabled="agentDebugging" @click="clearAgentPreview">清空</el-button></div>
</div> </div>
<div ref="agentPreviewChat" class="agent-preview-chat"> <div ref="agentPreviewChat" class="agent-preview-chat">

View File

@@ -1368,6 +1368,13 @@ textarea {
width: 100%; width: 100%;
} }
.agent-form-help {
margin-top: 6px;
color: #7b8d86;
font-size: 12px;
line-height: 1.6;
}
.debug-knowledge-select { width: 100%; min-width: 0; } .debug-knowledge-select { width: 100%; min-width: 0; }
.debug-knowledge-select .el-select__wrapper { height: 40px; min-height: 40px; overflow: hidden; } .debug-knowledge-select .el-select__wrapper { height: 40px; min-height: 40px; overflow: hidden; }
.debug-knowledge-select .el-select__selection { flex-wrap: nowrap; overflow: hidden; } .debug-knowledge-select .el-select__selection { flex-wrap: nowrap; overflow: hidden; }

View File

@@ -111,6 +111,7 @@ class AgentDebugHistoryMessage(BaseModel):
class AgentDebugRequest(BaseModel): class AgentDebugRequest(BaseModel):
promptContent: str = Field(min_length=1) promptContent: str = Field(min_length=1)
modelId: int = Field(gt=0) modelId: int = Field(gt=0)
userId: int | None = Field(default=None, gt=0)
knowledgeIds: list[int] = Field(default_factory=list) knowledgeIds: list[int] = Field(default_factory=list)
knowledgeVersions: dict[int, int] = Field(default_factory=dict) knowledgeVersions: dict[int, int] = Field(default_factory=dict)
question: str = Field(min_length=1, max_length=2000) question: str = Field(min_length=1, max_length=2000)

View File

@@ -6,14 +6,18 @@ from types import SimpleNamespace
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models.user import User
from app.models.admin import Admin from app.models.admin import Admin
from app.models.ai_config import ModelConfig from app.models.ai_config import ModelConfig
from app.schemas.admin import AgentDebugRequest from app.schemas.admin import AgentDebugRequest
from app.services.admin_service import OperationLogService from app.services.admin_service import OperationLogService
from app.services.entitlement_service import EntitlementService, entitlement_dict
from app.services.growth_profile_service import GrowthProfileService
from app.services.knowledge_agent_service import KnowledgeAgentService from app.services.knowledge_agent_service import KnowledgeAgentService
from app.services.model_stream_service import ModelStreamService from app.services.model_stream_service import ModelStreamService
from app.services.reasoning_policy_service import ReasoningPolicyService from app.services.reasoning_policy_service import ReasoningPolicyService
from app.services.rag_service import RagResult from app.services.rag_service import RagResult
from app.services.topic_session_service import TopicSessionService
class AgentDebugService: class AgentDebugService:
@@ -25,14 +29,18 @@ class AgentDebugService:
] ]
preview_knowledge_ids = payload.knowledgeIds or None preview_knowledge_ids = payload.knowledgeIds or None
version_overrides = payload.knowledgeVersions or None version_overrides = payload.knowledgeVersions or None
debug_context = AgentDebugService._debug_user_context(db, payload.userId)
rag_result = await KnowledgeAgentService.build_result( rag_result = await KnowledgeAgentService.build_result(
db, db,
question=payload.question, question=payload.question,
history=history, history=history,
version_overrides=version_overrides, version_overrides=version_overrides,
preview_knowledge_ids=preview_knowledge_ids, preview_knowledge_ids=preview_knowledge_ids,
user_id=payload.userId,
context_trace=debug_context["trace"],
prompt_override=payload.promptContent, prompt_override=payload.promptContent,
response_depth=payload.responseDepth, response_depth=payload.responseDepth,
growth_context=debug_context["growth_context"],
) )
return RagResult( return RagResult(
question=rag_result.question, question=rag_result.question,
@@ -45,6 +53,51 @@ class AgentDebugService:
messages=rag_result.messages, messages=rag_result.messages,
) )
@staticmethod
def _debug_user_context(db: Session, user_id: int | None) -> dict:
if user_id is None:
return {"growth_context": None, "trace": []}
user = db.get(User, user_id)
if user is None or user.is_deleted:
return {
"growth_context": None,
"trace": [
{
"tool": "load_debug_user_context",
"order": 1,
"request": {"userId": user_id},
"status": "failed",
"durationMs": 0,
"response": None,
"error": "模拟学员不存在或已删除",
}
],
}
entitlement = EntitlementService.active_entitlement(
db,
user,
monthly_topic_used=TopicSessionService.monthly_used_count(db, user.id),
)
growth_context = GrowthProfileService.prompt_context(db, user) if entitlement.enable_growth_profile else None
return {
"growth_context": growth_context,
"trace": [
{
"tool": "load_debug_user_context",
"order": 1,
"request": {"userId": user.id},
"status": "success",
"durationMs": 0,
"response": {
"userId": user.id,
"userName": user.name,
"entitlement": entitlement_dict(entitlement),
"growthProfileUsed": bool(growth_context),
},
}
],
}
@staticmethod @staticmethod
def overrides(payload: AgentDebugRequest) -> dict: def overrides(payload: AgentDebugRequest) -> dict:
return { return {

View File

@@ -0,0 +1,57 @@
from __future__ import annotations
import asyncio
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
from app.models import Base
from app.models.entitlement import EntitlementPlan
from app.models.growth import UserGrowthProfile
from app.models.user import User
from app.schemas.admin import AgentDebugRequest
from app.services.agent_debug_service import AgentDebugService
def _db() -> Session:
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool)
Base.metadata.create_all(engine)
return Session(engine)
def test_agent_debug_can_simulate_user_growth_profile_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(
EntitlementPlan(
id=10,
name="深度陪伴版",
plan_type="deep",
monthly_topic_limit=90,
enable_growth_profile=1,
status=1,
)
)
db.add(UserGrowthProfile(user_id=1, profile_text="用户在表达障碍主题上反复出现身体紧绷。"))
db.commit()
result = asyncio.run(
AgentDebugService.build_result(
db,
AgentDebugRequest(
promptContent="你是测试 Agent",
modelId=1,
userId=1,
question="我又表达不出来了怎么办",
),
)
)
rendered = "\n".join(item["content"] for item in result.messages)
assert "[长期成长档案]" in rendered
assert "表达障碍" in rendered
assert result.tool_trace[0]["tool"] == "load_debug_user_context"
assert result.tool_trace[0]["response"]["growthProfileUsed"] is True

View File

@@ -320,6 +320,8 @@ Agent 已经可以:
- 使用上下文摘要; - 使用上下文摘要;
- 记录检索日志; - 记录检索日志;
- 支持后台预览。 - 支持后台预览。
- 用户端正式聊天已按权益决定是否注入成长档案;
- 后台 Agent 预览已支持选择“模拟学员”,用于注入该学员权益和成长档案上下文。
但后续要让 Agent 区分: 但后续要让 Agent 区分:
@@ -353,6 +355,11 @@ Agent 已经可以:
- 基础版不会调用长期成长档案; - 基础版不会调用长期成长档案;
- 生成日志能看出本次用了哪些上下文。 - 生成日志能看出本次用了哪些上下文。
#### 开发进度
- 2026-07-31已补齐后台预览的“模拟学员”入口选择学员后预览链路会加载该学员权益、主题额度使用量和长期成长档案并在检索追踪中记录 `load_debug_user_context`。默认不选择学员时,保持原后台调试行为。
- 待继续:后台预览模拟具体主题上下文、求助卡/分享稿权限注入。
--- ---
### 05. 老师求助卡 ### 05. 老师求助卡
@@ -905,4 +912,3 @@ AI 日志增加:
1. 先只建权益版本和用户权益,不立刻改完整扣费; 1. 先只建权益版本和用户权益,不立刻改完整扣费;
2. 再建主题会话,只做记录不做复杂自动判断; 2. 再建主题会话,只做记录不做复杂自动判断;
3. 最后把主题会话接入权益扣减和成长档案。 3. 最后把主题会话接入权益扣减和成长档案。