feat: add agent response depth control

This commit is contained in:
2026-07-20 11:36:18 +08:00
parent 537978082a
commit c2655361b5
12 changed files with 192 additions and 5 deletions

View File

@@ -30,6 +30,7 @@ from app.services.feishu_service import FeishuKnowledgeService
from app.services.knowledge_service import KnowledgeScope
from app.services.model_service import ModelClientService
from app.services.reasoning_policy_service import ReasoningPolicyService
from app.services.response_style_service import ResponseStyleService
from app.services.secret_service import MASKED_SECRET, SENSITIVE_CONFIG_KEYS, SecretService
router = APIRouter()
@@ -206,7 +207,7 @@ def get_agent_runtime_config(
current_admin: Admin = Depends(get_current_admin),
) -> dict:
model = _enabled_model(db)
return api_success(_agent_runtime_config_dict(model, ReasoningPolicyService.is_visible(db)))
return api_success(_agent_runtime_config_dict(model, ReasoningPolicyService.is_visible(db), ResponseStyleService.get_depth(db)))
@router.put("/agent/runtime-config")
@@ -230,6 +231,7 @@ def save_agent_runtime_config(
model.stream_enabled = payload.streamEnabled
db.add(model)
ReasoningPolicyService.set_visible(db, payload.reasoningVisible == 1, current_admin.id)
ResponseStyleService.set_depth(db, payload.responseDepth, current_admin.id)
OperationLogService.write(
db,
admin_id=current_admin.id,
@@ -239,7 +241,7 @@ def save_agent_runtime_config(
)
db.commit()
db.refresh(model)
return api_success(_agent_runtime_config_dict(model, ReasoningPolicyService.is_visible(db)))
return api_success(_agent_runtime_config_dict(model, ReasoningPolicyService.is_visible(db), ResponseStyleService.get_depth(db)))
@router.get("/model/list")
@@ -443,7 +445,7 @@ def _enabled_model(db: Session) -> ModelConfig | None:
)
def _agent_runtime_config_dict(model: ModelConfig | None, reasoning_visible: bool = False) -> dict:
def _agent_runtime_config_dict(model: ModelConfig | None, reasoning_visible: bool = False, response_depth: int = 35) -> dict:
return {
"modelId": model.id if model is not None else None,
"modelName": (model.display_name or model.model_name) if model is not None else None,
@@ -455,6 +457,7 @@ def _agent_runtime_config_dict(model: ModelConfig | None, reasoning_visible: boo
"maxToken": model.max_token if model is not None and model.max_token is not None else 8192,
"streamEnabled": model.stream_enabled if model is not None else 1,
"reasoningVisible": 1 if reasoning_visible else 0,
"responseDepth": response_depth,
}

View File

@@ -98,6 +98,7 @@ class AgentDebugRequest(BaseModel):
maxToken: int | None = Field(default=8192, ge=1, le=100000)
streamEnabled: int = Field(default=1, ge=0, le=1)
reasoningVisible: int = Field(default=0, ge=0, le=1)
responseDepth: int = Field(default=35, ge=0, le=100)
class AgentRuntimeConfigSaveRequest(BaseModel):
@@ -109,6 +110,7 @@ class AgentRuntimeConfigSaveRequest(BaseModel):
maxToken: int = Field(default=8192, ge=256, le=100000)
streamEnabled: int = Field(default=1, ge=0, le=1)
reasoningVisible: int = Field(default=0, ge=0, le=1)
responseDepth: int = Field(default=35, ge=0, le=100)
class ModelSaveRequest(BaseModel):

View File

@@ -30,6 +30,7 @@ class AgentDebugService:
version_overrides=payload.knowledgeVersions,
preview_knowledge_ids=payload.knowledgeIds,
prompt_override=payload.promptContent,
response_depth=payload.responseDepth,
)
return RagResult(
question=rag_result.question,

View File

@@ -84,6 +84,7 @@ class KnowledgeAgentService:
preview_knowledge_ids: list[int] | None = None,
context_trace: list[dict] | None = None,
prompt_override: str | None = None,
response_depth: int | None = None,
) -> RagResult:
started = perf_counter()
catalog = cls.get_knowledge_catalog(
@@ -207,6 +208,7 @@ class KnowledgeAgentService:
session_summary,
summary_up_to_message_id,
prompt_override=prompt_override,
response_depth=response_depth,
)
return RagResult(
question=question,

View File

@@ -11,6 +11,7 @@ from app.models.user import User
from app.services.chat_context_service import ChatContextService
from app.services.feishu_service import FeishuKnowledgeService
from app.services.knowledge_service import KnowledgeAccessService, KnowledgeScope
from app.services.response_style_service import ResponseStyleService
NO_HIT_ANSWER = "当前知识库中未检索到相关内容,请联系管理员补充相关知识。"
@@ -119,6 +120,7 @@ class PromptService:
session_summary: str | None = None,
summary_up_to_message_id: int | None = None,
prompt_override: str | None = None,
response_depth: int | None = None,
) -> list[dict[str, str]]:
prompt = prompt_override.strip() if prompt_override and prompt_override.strip() else cls._load_active_prompt(db)
@@ -135,7 +137,8 @@ class PromptService:
+ "[不可关闭的最低安全规则 v1]\n"
+ "现实危险、自伤伤人风险应优先建议立即寻求线下专业帮助;医疗、法律、财务问题不得给出替代专业意见的结论;"
+ "不得伪造老师观点或课程内容;不得输出整篇课程文章、大段连续原文,也不得通过多轮拼接还原完整资料。"
+ cls._knowledge_type_rules(chunks),
+ cls._knowledge_type_rules(chunks)
+ ResponseStyleService.build_instruction(db, response_depth),
}
]

View File

@@ -0,0 +1,68 @@
from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.models.ai_config import SystemConfig
RESPONSE_DEPTH_KEY = "agent_response_depth"
DEFAULT_RESPONSE_DEPTH = 35
MIN_RESPONSE_DEPTH = 0
MAX_RESPONSE_DEPTH = 100
class ResponseStyleService:
@staticmethod
def get_depth(db: Session) -> int:
config = db.scalar(select(SystemConfig).where(SystemConfig.config_key == RESPONSE_DEPTH_KEY))
if config is None:
return DEFAULT_RESPONSE_DEPTH
try:
return _clamp_depth(int(config.config_value))
except (TypeError, ValueError):
return DEFAULT_RESPONSE_DEPTH
@staticmethod
def set_depth(db: Session, depth: int, admin_id: int | None) -> SystemConfig:
config = db.scalar(select(SystemConfig).where(SystemConfig.config_key == RESPONSE_DEPTH_KEY))
if config is None:
config = SystemConfig(config_key=RESPONSE_DEPTH_KEY, config_value=str(DEFAULT_RESPONSE_DEPTH))
config.config_value = str(_clamp_depth(depth))
config.description = "Agent 回答深度0 为精简100 为深入"
config.updated_by = admin_id
db.add(config)
return config
@classmethod
def build_instruction(cls, db: Session, depth: int | None = None) -> str:
value = _clamp_depth(cls.get_depth(db) if depth is None else depth)
if value <= 33:
mode = "精简"
rules = (
"回答要短通常控制在1到3段或3条以内不展开心理分析、不复盘学员长篇经历"
"优先给出一句方向确认,再引导回到当下、身体感受和情绪觉察。建议只给当下最小一步,"
"避免连续列出多个深入方案。"
)
elif value <= 66:
mode = "平衡"
rules = (
"回答保持适中可以先确认方向再补充必要原因和1到3个可执行提醒"
"不做过度拆解,不把分析过程完整罗列出来。"
)
else:
mode = "深入"
rules = (
"可以更完整地解释原因、步骤和注意事项,但仍要避免变成咨询式长篇分析;"
"优先围绕回到当下、回到自身、觉察情绪和身体感受、如是释放来组织内容。"
)
return (
"\n\n[回答深度控制]\n"
f"当前回答深度:{value}/100{mode})。{rules}"
"无论深度如何,都不要暴露内部分析过程;不要替学员做复杂人格判断;"
"涉及功课反馈时,先判断方向是否偏离,再给最少必要的下一步。"
)
def _clamp_depth(value: int) -> int:
return max(MIN_RESPONSE_DEPTH, min(MAX_RESPONSE_DEPTH, value))

View File

@@ -20,7 +20,7 @@ from app.services.model_stream_service import (
_stream_configured_model_async,
)
from app.services.model_service import ModelClientService, _copy_model_with_overrides, _max_output_tokens
from app.services.rag_service import RagResult
from app.services.rag_service import PromptService, RagResult
def _database() -> Session:
@@ -73,6 +73,7 @@ def test_runtime_config_defaults_to_long_answer_safe_max_tokens():
assert result["maxToken"] == 8192
assert result["streamEnabled"] == 1
assert result["reasoningVisible"] == 0
assert result["responseDepth"] == 35
assert _max_output_tokens(model) == 8192
@@ -97,6 +98,7 @@ def test_saved_runtime_config_is_persisted_on_enabled_model():
maxToken=12000,
streamEnabled=0,
reasoningVisible=1,
responseDepth=20,
),
db=db,
current_admin=admin,
@@ -113,7 +115,9 @@ def test_saved_runtime_config_is_persisted_on_enabled_model():
assert model.stream_enabled == 0
assert result["streamEnabled"] == 0
assert result["reasoningVisible"] == 1
assert result["responseDepth"] == 20
assert db.query(SystemConfig).filter_by(config_key="show_model_reasoning").one().config_value == "true"
assert db.query(SystemConfig).filter_by(config_key="agent_response_depth").one().config_value == "20"
payload = _openai_stream_payload(
model,
@@ -191,10 +195,24 @@ def test_debug_preview_passes_conversation_history_and_replaces_saved_prompt():
kwargs = build_result.await_args.kwargs
assert [item.content for item in kwargs["history"]] == ["最开始的问题", "第一次回答"]
assert kwargs["prompt_override"] == "调试主提示词"
assert kwargs["response_depth"] == 35
assert result.messages == rag_result.messages
assert result.prompt == "调试提示词渲染结果"
def test_prompt_includes_response_depth_instruction():
with _database() as db:
db.add(SystemConfig(config_key="agent_response_depth", config_value="20"))
db.commit()
messages = PromptService.build_messages(db, "做功课没反应怎么办?", [])
system = messages[0]["content"]
assert "回答深度20/100精简" in system
assert "不展开心理分析" in system
assert "回到当下、身体感受和情绪觉察" in system
@pytest.mark.parametrize("reasoning_visible", [True, False])
def test_agent_debug_stream_respects_reasoning_visibility(reasoning_visible):
async def chunks():