模型选择器在哪里

摘要

在使用各类 AI 对话产品(如 ChatGPT、Claude、通义千问、DeepSeek 等)时,很多用户都会遇到一个共同的困惑:“我想换一个模型,但模型选择器到底在哪里?” 本文从对话界面底部的切换入口出发,系统梳理模型选择器的常见位置、交互路径、状态管理逻辑,并给出前端实现一个"底部模型切换器"的完整代码示例,帮助开发者和用户 alike 彻底搞清楚这个看似简单却经常让人迷路的 UI 元素。

引言

随着大模型能力的细分,一个 AI 产品往往同时提供多个模型:快速模型、推理模型、长上下文模型、多模态模型……用户需要在对话过程中随时切换。于是"模型选择器"(Model Selector / Model Switcher)成为对话界面的标配组件。

但奇怪的是,这个组件的位置并不统一:

  • 有的产品把它放在输入框左下角,一个小小的下拉按钮;
  • 有的放在输入框右下角,和发送按钮并排;
  • 有的藏在顶部标题栏,需要点击当前模型名才能展开;
  • 还有的干脆收进了"设置"里,让用户找半天。

本文的核心观点是:对于绝大多数现代对话产品,模型选择器最标准、最高频的位置就是对话输入区(底部)的切换入口。理解这个入口的设计逻辑与操作路径,既能帮助用户快速上手,也能帮助前端开发者正确实现它。

下面我们分多个小节展开。


一、为什么模型选择器通常在"底部"

要回答"在哪里",先要回答"为什么在那里"。

对话界面的视觉重心是消息流,而用户的操作重心是输入框。模型选择是一个"高频但轻量"的操作——用户可能在一轮对话中切换两三次,但它不像发送消息那样是核心动作。因此产品设计上通常遵循以下原则:

  1. 靠近操作点:切换模型的目的通常是"接下来这条消息用哪个模型回答",所以选择器应该紧贴输入框。
  2. 不打断阅读:放在顶部会与标题栏、会话列表争夺空间;放在底部输入区则天然属于"输入动作"的一部分。
  3. 状态可见:用户需要随时知道"当前用的是哪个模型",底部常驻的标签比折叠菜单更直观。

这就解释了为什么你在大多数产品里,目光往输入框下方或内部一瞥,就能找到模型选择器。


二、对话界面底部的三种典型入口形态

虽然都在底部,但具体形态有差异。常见的有三类:

2.1 输入框内左下角的下拉按钮

这是最常见的一种。输入框内部左侧有一个带模型名的小胶囊按钮,点击后向上弹出模型列表。

┌─────────────────────────────────────────────┐
│  请描述你的问题……                            │
│                                             │
│  [ GPT-4o ▾ ]                    [ 发送 ]   │
└─────────────────────────────────────────────┘
        ↑
   模型选择器入口

2.2 输入框下方的工具条

部分产品把模型选择器放在输入框外部下方的一行工具条里,与"联网搜索""上传文件"等按钮并列。

┌─────────────────────────────────────────────┐
│  请描述你的问题……                            │
└─────────────────────────────────────────────┘
  [ 模型:Claude ▾ ]  [ 🌐 联网 ]  [ 📎 上传 ]

2.3 输入框右下角的图标按钮

移动端或极简风格产品中,模型选择器可能被压缩成一个图标(如 ✨ 或 ⚙),点击后从底部弹出抽屉(Bottom Sheet)。

无论哪种形态,操作路径都是一致的:定位输入区 → 找到当前模型标识 → 点击展开 → 选择目标模型 → 收起并生效。


三、完整的操作路径拆解

以最常见的"左下角下拉"为例,一次完整的切换路径如下:

步骤用户动作界面反馈
1视线移到输入框左下角显示当前模型名,如 GPT-4o
2点击该按钮向上弹出模型列表,当前项高亮
3浏览列表每项显示模型名、简介、能力标签
4点击目标模型列表收起,按钮文字更新
5发送下一条消息请求体中携带新模型标识

关键细节在于第 4 步的状态同步:选择器不仅要更新自己的显示,还要把新模型写入全局状态,供后续请求使用。这也是很多实现出 bug 的地方——UI 变了,但请求还发的是旧模型。


四、用代码实现一个底部模型选择器

下面用 React + TypeScript 实现一个可运行的底部模型选择器,覆盖"展开、选择、状态同步"的完整逻辑。

4.1 模型数据结构与全局状态

// types.ts
export interface ModelOption {
  id: string;          // 请求时使用的模型标识
  name: string;        // 展示名称
  description: string; // 一句话简介
  tag?: string;        // 能力标签,如 "推理"、"长文本"
}

export const MODEL_OPTIONS: ModelOption[] = [
  { id: 'gpt-4o',      name: 'GPT-4o',      description: '通用快速模型', tag: '推荐' },
  { id: 'o1-preview',  name: 'o1-preview',  description: '深度推理模型', tag: '推理' },
  { id: 'gpt-4-turbo', name: 'GPT-4 Turbo', description: '超长上下文',   tag: '长文本' },
];

4.2 选择器组件

// ModelSelector.tsx
import React, { useState, useRef, useEffect } from 'react';
import { MODEL_OPTIONS, ModelOption } from './types';

interface Props {
  value: string;                       // 当前模型 id
  onChange: (id: string) => void;      // 切换回调
}

export const ModelSelector: React.FC<Props> = ({ value, onChange }) => {
  const [open, setOpen] = useState(false);
  const wrapperRef = useRef<HTMLDivElement>(null);

  // 当前选中的模型对象,用于展示名称
  const current: ModelOption =
    MODEL_OPTIONS.find((m) => m.id === value) ?? MODEL_OPTIONS[0];

  // 点击外部关闭弹层
  useEffect(() => {
    const handleClickOutside = (e: MouseEvent) => {
      if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
        setOpen(false);
      }
    };
    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, []);

  const handleSelect = (id: string) => {
    onChange(id);   // 同步到全局状态
    setOpen(false); // 收起弹层
  };

  return (
    <div className="model-selector" ref={wrapperRef}>
      {/* 触发按钮:位于输入框左下角 */}
      <button
        type="button"
        className="model-trigger"
        aria-haspopup="listbox"
        aria-expanded={open}
        onClick={() => setOpen((v) => !v)}
      >
        <span className="model-name">{current.name}</span>
        <span className={`arrow ${open ? 'up' : 'down'}`}>▾</span>
      </button>

      {/* 向上弹出的模型列表 */}
      {open && (
        <ul className="model-list" role="listbox">
          {MODEL_OPTIONS.map((m) => (
            <li
              key={m.id}
              role="option"
              aria-selected={m.id === value}
              className={`model-item ${m.id === value ? 'active' : ''}`}
              onClick={() => handleSelect(m.id)}
            >
              <div className="item-head">
                <span className="item-name">{m.name}</span>
                {m.tag && <span className="item-tag">{m.tag}</span>}
              </div>
              <div className="item-desc">{m.description}</div>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

4.3 输入区整合与请求携带模型

// ChatInput.tsx
import React, { useState } from 'react';
import { ModelSelector } from './ModelSelector';

export const ChatInput: React.FC = () => {
  const [model, setModel] = useState('gpt-4o');
  const [text, setText] = useState('');

  const handleSend = async () => {
    if (!text.trim()) return;

    // 关键:请求体中携带当前选中的模型
    const res = await fetch('/api/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ model, message: text }),
    });

    if (!res.ok) {
      console.error('请求失败', res.status);
      return;
    }
    setText('');
  };

  return (
    <div className="chat-input">
      <textarea
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="请描述你的问题……"
      />
      {/* 底部工具条:左侧模型选择器,右侧发送按钮 */}
      <div className="input-toolbar">
        <ModelSelector value={model} onChange={setModel} />
        <button className="send-btn" onClick={handleSend}>
          发送
        </button>
      </div>
    </div>
  );
};

4.4 配套样式(保证弹层向上展开)

/* model-selector.css */
.model-selector {
  position: relative;   /* 弹层定位基准 */
  display: inline-block;
}

.model-trigger {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  border: 1px solid #ddd;
  border-radius: 16px;
  background: #fff;
  cursor: pointer;
}

/* 关键:弹层向上展开,避免被输入框遮挡 */
.model-list {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  width: 260px;
  margin: 0;
  padding: 6px;
  list-style: none;
  background: #fff;
  border: 1px solid #e5e5e5;
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}

.model-item {
  padding: 8px 10px;
  border-radius: 8px;
  cursor: pointer;
}

.model-item:hover,
.model-item.active {
  background: #f2f6ff;
}

.item-tag {
  margin-left: 6px;
  padding: 0 6px;
  font-size: 12px;
  color: #2f6bff;
  background: #e8f0ff;
  border-radius: 4px;
}

以上代码可直接运行于任意 React + TypeScript 工程。核心要点有三个:弹层用 bottom: 100% 向上展开、点击外部自动关闭、选择后通过 onChange 同步到全局状态并随请求发送。


五、找不到模型选择器时的排查清单

作为用户,如果你在某个产品里找不到模型选择器,可以按以下顺序排查:

  1. 看输入框内部:左下角是否有带模型名的小按钮?很多产品默认折叠,只显示当前模型。
  2. 看输入框下方:是否有工具条,模型名与"联网""上传"等按钮并列?
  3. 看顶部标题栏:部分产品(尤其是移动端)把模型名放在会话标题旁,点击可切换。
  4. 看设置页:少数产品把默认模型放在"设置 → 模型"里,切换需进入设置。
  5. 确认权限:部分企业版或教育版账号被限制了模型切换权限,选择器会被直接隐藏。

作为开发者,如果你实现的选择器"用户找不到",多半是可见性不足:按钮没有显示当前模型名、颜色与背景对比度太低,或者被输入框遮挡。建议始终在触发按钮上展示当前模型名称,而不是一个抽象图标。


六、设计建议与常见坑

  1. 始终展示当前模型名,不要只放一个 ✨ 图标,否则用户不知道自己在用什么模型。
  2. 弹层方向要正确:底部选择器的弹层必须向上展开,否则会被屏幕边缘裁切。
  3. 状态单一来源:模型 id 只应存在于一个全局状态里,选择器和请求逻辑都从它读取,避免"UI 显示 A、请求发送 B"。
  4. 切换后给出轻提示:例如在消息流中插入一条"已切换到 GPT-4o"的系统提示,让用户确认切换生效。
  5. 移动端用底部抽屉:小屏下向上弹层空间不足,改用 Bottom Sheet 更符合手势习惯。

总结

回到最初的问题:模型选择器在哪里?

答案是——它几乎总是位于对话界面底部的输入区:要么在输入框内部左下角,要么在输入框下方的工具条中,移动端则常以图标形式出现、点击后弹出底部抽屉。它的操作路径高度统一:定位当前模型标识 → 点击展开列表 → 选择目标模型 → 状态同步并随下一条请求发送。

对于用户,记住"往输入框附近找"就能快速定位;对于开发者,本文给出的 React + TypeScript 示例覆盖了数据定义、弹层交互、状态同步与请求携带的完整链路,可直接作为实现参考。一个小小的底部切换入口,背后是可见性设计、状态管理与请求契约三者的协同——把它做对,用户体验会顺畅很多。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐