feat(reasoning): support operator-configurable default mode via DOBOTAI_DEFAULT_MODE
This commit is contained in:
@@ -79,10 +79,22 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defaultMode := reasoningmode.ModeQuick
|
||||
if raw := strings.TrimSpace(os.Getenv("DOBOTAI_DEFAULT_MODE")); raw != "" {
|
||||
switch raw {
|
||||
case "reasoning":
|
||||
defaultMode = reasoningmode.ModeReasoning
|
||||
case "quick":
|
||||
defaultMode = reasoningmode.ModeQuick
|
||||
default:
|
||||
log.Fatal("DOBOTAI_DEFAULT_MODE must be quick or reasoning")
|
||||
}
|
||||
}
|
||||
reasoningService, err := reasoningmode.NewService(
|
||||
store,
|
||||
os.Getenv("DOBOTAI_QUICK_AGENT_ID"),
|
||||
os.Getenv("DOBOTAI_REASONING_AGENT_ID"),
|
||||
reasoningmode.WithDefaultMode(defaultMode),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE reasoning_mode_states
|
||||
DROP COLUMN user_selected;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 000022_reasoning_default_mode: support an operator-configurable default
|
||||
-- mode for the /think reasoning router. The new column records whether a
|
||||
-- user has explicitly chosen a mode (/think on|off); users who never have
|
||||
-- will follow DOBOTAI_DEFAULT_MODE instead of the hardcoded quick answer.
|
||||
ALTER TABLE reasoning_mode_states
|
||||
ADD COLUMN user_selected BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -49,11 +49,28 @@ type Service struct {
|
||||
repo Repository
|
||||
quickAgentID string
|
||||
reasoningAgentID string
|
||||
defaultMode Mode
|
||||
now func() time.Time
|
||||
idleTimeout time.Duration
|
||||
}
|
||||
|
||||
func NewService(repo Repository, quickAgentID, reasoningAgentID string) (*Service, error) {
|
||||
// Option customises a reasoning mode Service.
|
||||
type Option func(*Service) error
|
||||
|
||||
// WithDefaultMode sets the mode applied to users who never explicitly chose
|
||||
// one with /think. Passing ModeQuick (the default when unset) preserves the
|
||||
// historical behaviour of answering with the quick agent.
|
||||
func WithDefaultMode(mode Mode) Option {
|
||||
return func(s *Service) error {
|
||||
if mode != ModeQuick && mode != ModeReasoning {
|
||||
return fmt.Errorf("invalid default reasoning mode %q", mode)
|
||||
}
|
||||
s.defaultMode = mode
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewService(repo Repository, quickAgentID, reasoningAgentID string, opts ...Option) (*Service, error) {
|
||||
quickAgentID = strings.TrimSpace(quickAgentID)
|
||||
reasoningAgentID = strings.TrimSpace(reasoningAgentID)
|
||||
if repo == nil {
|
||||
@@ -65,13 +82,29 @@ func NewService(repo Repository, quickAgentID, reasoningAgentID string) (*Servic
|
||||
if quickAgentID == reasoningAgentID {
|
||||
return nil, errors.New("quick and reasoning agent IDs must differ")
|
||||
}
|
||||
return &Service{
|
||||
service := &Service{
|
||||
repo: repo,
|
||||
quickAgentID: quickAgentID,
|
||||
reasoningAgentID: reasoningAgentID,
|
||||
defaultMode: ModeQuick,
|
||||
now: time.Now,
|
||||
idleTimeout: defaultIdleTimeout,
|
||||
}, nil
|
||||
}
|
||||
for _, opt := range opts {
|
||||
if err := opt(service); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// effectivelyReasoning reports whether a turn should be served by the
|
||||
// reasoning agent given the resolved state and the configured default mode.
|
||||
func (s *Service) effectivelyReasoning(state State) bool {
|
||||
if state.Mode == ModeReasoning {
|
||||
return true
|
||||
}
|
||||
return !state.UserSelected && s.defaultMode == ModeReasoning
|
||||
}
|
||||
|
||||
func (s *Service) Prepare(ctx context.Context, scope Scope, text string) (Decision, error) {
|
||||
@@ -95,10 +128,14 @@ func (s *Service) Prepare(ctx context.Context, scope Scope, text string) (Decisi
|
||||
state.MaxRounds,
|
||||
)}, nil
|
||||
case CommandDisable:
|
||||
_, wasReasoning, err := s.repo.Disable(ctx, scope, s.now())
|
||||
previous, _, err := s.repo.ResolveState(ctx, scope, s.now())
|
||||
if err != nil {
|
||||
return Decision{}, err
|
||||
}
|
||||
wasReasoning := s.effectivelyReasoning(previous)
|
||||
if _, _, err = s.repo.Disable(ctx, scope, s.now()); err != nil {
|
||||
return Decision{}, err
|
||||
}
|
||||
if !wasReasoning {
|
||||
return Decision{Handled: true, Reply: "当前未开启推理模式,默认是快速问答模式。"}, nil
|
||||
}
|
||||
@@ -106,7 +143,7 @@ func (s *Service) Prepare(ctx context.Context, scope Scope, text string) (Decisi
|
||||
case CommandSingle:
|
||||
return Decision{Query: command.Query, AgentID: s.reasoningAgentID, Execution: ExecutionReasoningSingle}, nil
|
||||
case CommandHelp:
|
||||
return Decision{Handled: true, Reply: helpReply()}, nil
|
||||
return Decision{Handled: true, Reply: helpReply(s.defaultMode)}, nil
|
||||
default:
|
||||
return Decision{Handled: true, Reply: "用法:/think <问题>、/think on、/think off。"}, nil
|
||||
}
|
||||
@@ -119,13 +156,25 @@ func (s *Service) Prepare(ctx context.Context, scope Scope, text string) (Decisi
|
||||
if state.Mode == ModeReasoning {
|
||||
return Decision{Query: query, AgentID: s.reasoningAgentID, Execution: ExecutionReasoning}, nil
|
||||
}
|
||||
// The user never chose a mode explicitly: follow the operator default.
|
||||
// Default-mode reasoning runs as a single reasoning turn so it never
|
||||
// enters the rounds/idle-expiry machinery; the user can pin quick mode
|
||||
// any time with /think off.
|
||||
if s.effectivelyReasoning(state) {
|
||||
return Decision{Query: query, AgentID: s.reasoningAgentID, Execution: ExecutionReasoningSingle}, nil
|
||||
}
|
||||
return Decision{Query: query, AgentID: s.quickAgentID, Execution: ExecutionQuick, Expired: expired}, nil
|
||||
}
|
||||
|
||||
func helpReply() string {
|
||||
func helpReply(defaultMode Mode) string {
|
||||
defaultLabel := "默认模式:快速问答模式,基于知识库快速准确作答,响应快、成本低,适合信息查询和事实确认。\n" +
|
||||
"智能推理模式:支持多步思考与工具调用,计费标准更高,适合复杂分析和推导。\n"
|
||||
if defaultMode == ModeReasoning {
|
||||
defaultLabel = "默认模式:智能推理模式,支持多步思考与工具调用,效果更强、计费标准更高。\n" +
|
||||
"快速问答模式:基于知识库快速准确作答,响应快、成本低,可用 /think off 切换并保持。\n"
|
||||
}
|
||||
return "一、模式说明\n" +
|
||||
"默认模式:快速问答模式,基于知识库快速准确作答,响应快、成本低,适合信息查询和事实确认。\n" +
|
||||
"智能推理模式:支持多步思考与工具调用,计费标准更高,适合复杂分析和推导。\n\n" +
|
||||
defaultLabel + "\n" +
|
||||
"二、模式切换指令\n" +
|
||||
"/think + 你的问题:仅当前问题使用智能推理\n" +
|
||||
"/think on:开启持续智能推理模式\n" +
|
||||
|
||||
@@ -22,6 +22,7 @@ var ErrInvalidState = errors.New("invalid reasoning mode state")
|
||||
|
||||
type State struct {
|
||||
Mode Mode
|
||||
UserSelected bool
|
||||
MaxRounds int
|
||||
RoundsUsed int
|
||||
HintEnabled bool
|
||||
|
||||
@@ -61,6 +61,7 @@ func (s *Store) Enable(ctx context.Context, scope reasoningmode.Scope, now time.
|
||||
return reasoningmode.State{}, err
|
||||
}
|
||||
state.Mode = reasoningmode.ModeReasoning
|
||||
state.UserSelected = true
|
||||
state.RoundsUsed = 0
|
||||
state.LastActivityAt = now
|
||||
state.ExpiresAt = now.Add(idle)
|
||||
@@ -92,6 +93,7 @@ func (s *Store) Disable(ctx context.Context, scope reasoningmode.Scope, now time
|
||||
state, _ = reasoningmode.EffectiveState(state, now)
|
||||
wasReasoning := state.Mode == reasoningmode.ModeReasoning
|
||||
state.Mode = reasoningmode.ModeQuick
|
||||
state.UserSelected = true
|
||||
state.RoundsUsed = 0
|
||||
state.ExpiresAt = time.Time{}
|
||||
if err = updateReasoningState(ctx, tx, scope, state, now); err != nil {
|
||||
@@ -256,14 +258,14 @@ func ensureReasoningState(ctx context.Context, tx pgx.Tx, scope reasoningmode.Sc
|
||||
}
|
||||
|
||||
func selectReasoningStateForUpdate(ctx context.Context, tx pgx.Tx, scope reasoningmode.Scope) (reasoningmode.State, error) {
|
||||
return scanReasoningState(tx.QueryRow(ctx, `SELECT mode,max_rounds,rounds_used,hint_enabled,last_activity_at,expires_at,onboarding_shown_at
|
||||
return scanReasoningState(tx.QueryRow(ctx, `SELECT mode,user_selected,max_rounds,rounds_used,hint_enabled,last_activity_at,expires_at,onboarding_shown_at
|
||||
FROM reasoning_mode_states
|
||||
WHERE tenant_key=$1 AND channel=$2 AND external_actor_id=$3
|
||||
FOR UPDATE`, scope.TenantKey, scope.Channel, scope.ExternalActorID))
|
||||
}
|
||||
|
||||
func selectReasoningState(ctx context.Context, tx pgx.Tx, scope reasoningmode.Scope) (reasoningmode.State, error) {
|
||||
return scanReasoningState(tx.QueryRow(ctx, `SELECT mode,max_rounds,rounds_used,hint_enabled,last_activity_at,expires_at,onboarding_shown_at
|
||||
return scanReasoningState(tx.QueryRow(ctx, `SELECT mode,user_selected,max_rounds,rounds_used,hint_enabled,last_activity_at,expires_at,onboarding_shown_at
|
||||
FROM reasoning_mode_states
|
||||
WHERE tenant_key=$1 AND channel=$2 AND external_actor_id=$3`, scope.TenantKey, scope.Channel, scope.ExternalActorID))
|
||||
}
|
||||
@@ -276,7 +278,7 @@ func scanReasoningState(row pgx.Row) (reasoningmode.State, error) {
|
||||
expires pgtype.Timestamptz
|
||||
onboarding pgtype.Timestamptz
|
||||
)
|
||||
if err := row.Scan(&mode, &state.MaxRounds, &state.RoundsUsed, &state.HintEnabled, &last, &expires, &onboarding); err != nil {
|
||||
if err := row.Scan(&mode, &state.UserSelected, &state.MaxRounds, &state.RoundsUsed, &state.HintEnabled, &last, &expires, &onboarding); err != nil {
|
||||
return reasoningmode.State{}, fmt.Errorf("scan reasoning state: %w", err)
|
||||
}
|
||||
state.Mode = reasoningmode.Mode(mode)
|
||||
@@ -295,11 +297,11 @@ func scanReasoningState(row pgx.Row) (reasoningmode.State, error) {
|
||||
|
||||
func updateReasoningState(ctx context.Context, tx pgx.Tx, scope reasoningmode.Scope, state reasoningmode.State, now time.Time) error {
|
||||
_, err := tx.Exec(ctx, `UPDATE reasoning_mode_states SET
|
||||
mode=$4,max_rounds=$5,rounds_used=$6,hint_enabled=$7,
|
||||
last_activity_at=$8,expires_at=$9,onboarding_shown_at=$10,updated_at=$11
|
||||
mode=$4,user_selected=$5,max_rounds=$6,rounds_used=$7,hint_enabled=$8,
|
||||
last_activity_at=$9,expires_at=$10,onboarding_shown_at=$11,updated_at=$12
|
||||
WHERE tenant_key=$1 AND channel=$2 AND external_actor_id=$3`,
|
||||
scope.TenantKey, scope.Channel, scope.ExternalActorID,
|
||||
string(state.Mode), state.MaxRounds, state.RoundsUsed, state.HintEnabled,
|
||||
string(state.Mode), state.UserSelected, state.MaxRounds, state.RoundsUsed, state.HintEnabled,
|
||||
nullableTime(state.LastActivityAt), nullableTime(state.ExpiresAt), nullableTime(state.OnboardingShownAt), now,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user