feat(agent): control reasoning visibility

This commit is contained in:
2026-07-17 14:05:54 +08:00
parent bfaf2ebf67
commit a879dc2ff1
18 changed files with 368 additions and 51 deletions

View File

@@ -3,6 +3,7 @@ from decimal import Decimal
import json
from unittest.mock import patch
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool
@@ -10,7 +11,7 @@ from sqlalchemy.pool import StaticPool
from app.api.admin_settings import _debug_agent_stream, get_agent_runtime_config, save_agent_runtime_config
from app.models import Base
from app.models.admin import Admin
from app.models.ai_config import ModelConfig
from app.models.ai_config import ModelConfig, SystemConfig
from app.schemas.admin import AgentDebugRequest, AgentRuntimeConfigSaveRequest
from app.services.model_stream_service import (
AsyncStreamingModelResponse,
@@ -57,7 +58,11 @@ def test_runtime_config_defaults_to_long_answer_safe_max_tokens():
with _database() as db:
admin = _admin()
model = _model()
db.add_all([admin, model])
db.add_all([
admin,
model,
SystemConfig(id=1, config_key="show_model_reasoning", config_value="false"),
])
db.commit()
result = get_agent_runtime_config(db=db, current_admin=admin)["data"]
@@ -66,6 +71,7 @@ def test_runtime_config_defaults_to_long_answer_safe_max_tokens():
assert result["modelName"] == "正式模型"
assert result["maxToken"] == 8192
assert result["streamEnabled"] == 1
assert result["reasoningVisible"] == 0
assert _max_output_tokens(model) == 8192
@@ -73,7 +79,11 @@ def test_saved_runtime_config_is_persisted_on_enabled_model():
with _database() as db:
admin = _admin()
model = _model()
db.add_all([admin, model])
db.add_all([
admin,
model,
SystemConfig(id=1, config_key="show_model_reasoning", config_value="false"),
])
db.commit()
result = save_agent_runtime_config(
@@ -85,6 +95,7 @@ def test_saved_runtime_config_is_persisted_on_enabled_model():
frequencyPenalty=0.4,
maxToken=12000,
streamEnabled=0,
reasoningVisible=1,
),
db=db,
current_admin=admin,
@@ -100,6 +111,8 @@ def test_saved_runtime_config_is_persisted_on_enabled_model():
assert model.frequency_penalty == Decimal("0.40")
assert model.stream_enabled == 0
assert result["streamEnabled"] == 0
assert result["reasoningVisible"] == 1
assert db.query(SystemConfig).filter_by(config_key="show_model_reasoning").one().config_value == "true"
payload = _openai_stream_payload(
model,
@@ -133,7 +146,8 @@ def test_disabled_stream_returns_one_complete_chunk():
assert chunks == [answer]
def test_agent_debug_stream_emits_status_content_and_trace():
@pytest.mark.parametrize("reasoning_visible", [True, False])
def test_agent_debug_stream_respects_reasoning_visibility(reasoning_visible):
async def chunks():
yield "<think>内部思考</think>"
yield "- **第一步**:停一下"
@@ -162,6 +176,11 @@ def test_agent_debug_stream_emits_status_content_and_trace():
admin = _admin()
model = _model()
db.add_all([admin, model])
db.add(SystemConfig(
id=1,
config_key="show_model_reasoning",
config_value="true" if reasoning_visible else "false",
))
db.commit()
payload = AgentDebugRequest(
promptContent="你是测试助手",
@@ -187,10 +206,18 @@ def test_agent_debug_stream_emits_status_content_and_trace():
for event in events
if event != "data: [DONE]\n\n"
]
assert [event["type"] for event in decoded] == ["status", "content", "content", "complete"]
assert decoded[1]["content"].startswith("<think>")
assert decoded[2]["content"] == "- **第一步**:停一下"
assert decoded[3]["retrievalTrace"][0]["tool"] == "KnowledgeSearch"
expected_types = (
["status", "reasoning", "content", "complete"]
if reasoning_visible
else ["status", "content", "complete"]
)
assert [event["type"] for event in decoded] == expected_types
assert decoded[0]["reasoningVisible"] is reasoning_visible
if reasoning_visible:
assert decoded[1]["content"] == "内部思考"
content_event = next(event for event in decoded if event["type"] == "content")
assert content_event["content"] == "- **第一步**:停一下"
assert decoded[-1]["retrievalTrace"][0]["tool"] == "KnowledgeSearch"
def test_agent_debug_does_not_truncate_long_answer(monkeypatch):

View File

@@ -101,6 +101,7 @@ def test_queued_chat_reports_position_and_completes(monkeypatch):
monkeypatch.setattr(chat, "wait_for_chat_slot", wait_slot)
monkeypatch.setattr(chat, "release_chat_slot", release_slot)
monkeypatch.setattr(chat.ChatStreamService, "stream_answer_async", stream_answer)
monkeypatch.setattr(chat.ReasoningPolicyService, "is_visible", lambda _db: False)
payload = type("Payload", (), {"sessionId": 1, "message": "问题"})()
async def collect_events():

View File

@@ -0,0 +1,18 @@
from app.services.reasoning_policy_service import ReasoningPolicyService, ReasoningStreamParser
def test_reasoning_parser_handles_tags_split_across_chunks():
parser = ReasoningStreamParser()
segments = []
for chunk in ["<thi", "nk>内部", "思考</th", "ink>## 回答"]:
segments.extend(parser.feed(chunk))
segments.extend(parser.finish())
assert "".join(item.content for item in segments if item.kind == "reasoning") == "内部思考"
assert "".join(item.content for item in segments if item.kind == "content") == "## 回答"
def test_reasoning_is_removed_from_history_text_when_policy_is_off():
text = "<think>不应透出</think>\n正式回答"
assert ReasoningPolicyService.strip_reasoning(text) == "\n正式回答"