ChatComposer 输入区改造:基于技能市场实现 Agent 输入框

ChatComposer 输入区改造:基于技能市场实现 Agent 输入框

前言

前面已经把 AI 工作台中间区域拆成了几个组件:

text 复制代码
ChatWorkspace.vue
ChatHeader.vue
ChatMessageList.vue
ChatComposer.vue

其中 ChatComposer 负责底部输入区。

在普通聊天产品里,输入区通常只需要一个输入框和发送按钮。

但在 Agent 产品里,输入区不只是让用户输入一句话,它还需要告诉系统:

用户这次想让 AI 用什么能力来处理这个任务。

所以这次我对 ChatComposer 做了一个小改造:在输入框里加入"技能市场"。

当前效果大概是:

发送时,前端不再只提交用户输入的文字,而是提交:

ts 复制代码
{
  message: '帮我生成一个科技企业官网',
  skillKeys: ['generate-page', 'optimize-style'],
  skills: [...]
}

这样它就更接近真实生产环境里的 Agent 输入框。

什么是Skill

这里的"技能"可以理解成 AI Agent 的一种能力。

普通聊天输入框只关心:

text 复制代码
用户输入了什么

比如:

text 复制代码
帮我生成一个官网

但 Agent 产品通常还需要关心:

text 复制代码
用户想用什么Skill处理这个需求

比如:

text 复制代码
生成网页
优化样式
拆分组件
修复问题
生成文档

同样一句话,在不同技能下,AI 的处理方式是不一样的。

比如用户输入:

text 复制代码
帮我优化首页

如果选择的是:

text 复制代码
优化样式

AI 可能更关注页面间距、颜色、布局和视觉层级。

如果选择的是:

text 复制代码
拆分组件

AI 可能更关注组件结构、文件拆分和代码可维护性。

所以技能的作用是:

给用户输入补充一个任务上下文,让 AI 更清楚这次应该按什么能力处理。

生产环境里的技能不只是一个按钮

一开始做原型时,技能很容易被写成这样:

ts 复制代码
{
  label: '生成网页',
  value: 'webpage'
}

这种写法可以展示 UI,但还不太像真实项目。

真实生产环境里,一个技能通常应该是一份结构化配置。

它至少应该包含:

字段 作用
key 技能唯一标识,后续传给后端或 Agent 调度层
name 技能名称,用于页面展示
description 技能说明,用于下拉菜单解释
category 技能分类,后续方便分组和筛选
enabled 是否启用,可用于权限控制
icon 技能图标
prompt 技能对应的系统提示词
placeholder 选中技能后的输入框提示语
tags 标签,后续可以用于搜索或分类
defaultSelected 是否默认选中

所以我把技能从 ChatComposer.vue 里抽了出来,单独放到配置文件里。

对应文件:

text 复制代码
src/features/workbench/config/skills.ts

Ant Design X React 版是怎么做的

在 Ant Design X 的 React 版中,Sender 已经内置了和技能相关的能力。

官方文档里,Sender 支持:

text 复制代码
skill
slotConfig

其中:

  • skill 表示当前技能配置;
  • slotConfig 表示词槽配置;
  • 配置后输入框会进入词槽模式;
  • 提交时可以拿到用户输入、词槽配置和技能信息。

React 版文档里的 onSubmit 类型大致是:

ts 复制代码
(message: string, slotConfig: SlotConfigType[], skill: SkillType) => void

也就是说,React 版的 Sender 提交时不是只给一个字符串。

它会同时给出:

text 复制代码
message      用户输入内容
slotConfig   结构化词槽内容
skill        当前技能

官方 SkillType 大致是:

ts 复制代码
interface SkillType {
  title?: React.ReactNode;
  value: string;
  toolTip?: TooltipProps;
  closable?:
    | boolean
    | {
        closeIcon?: React.ReactNode;
        onClose?: React.MouseEventHandler<HTMLDivElement>;
        disabled?: boolean;
      };
}

它最核心的字段是:

text 复制代码
title:展示给用户看的技能名称
value:真正提交时使用的技能值

slotConfig 更偏结构化输入。

比如官方示例中类似:

text 复制代码
Writing Assistant × Please write an article about [Please enter a topic]. The requirement is [800] words.

这里的 [Please enter a topic][800] 就不是普通文本,而是词槽。

所以 React 版的能力更完整,它不是简单画一个技能标签,而是把技能和结构化输入都放进了 Sender 内部。

Vue 版当前的问题

我当前项目使用的是:

text 复制代码
ant-design-x-vue@1.6.0

我查了本地源码和文档,当前 Vue 版 Sender 暂时没有 React 版那种:

ts 复制代码
skill
slotConfig

它现在主要支持:

ts 复制代码
prefix
footer
header
actions
loading
placeholder
onSubmit
onChange
onCancel
onPasteFile

也就是说,不能直接这样写:

vue 复制代码
<Sender
  :skill="skill"
  :slot-config="slotConfig"
/>

如果这样写,当前 Vue 版并不会识别这些属性。

所以我的做法是:

UI 上先使用 Sender 已有的 prefixfooter 插槽实现技能市场,数据层再设计完整的 AgentSkill 配置和提交 payload。

这样虽然不是组件库原生 skill API,但业务上已经具备生产环境需要的技能上下文。

当前项目怎么实现

当前输入区对应文件是:

text 复制代码
src/features/workbench/components/chat/ChatComposer.vue

技能配置对应文件是:

text 复制代码
src/features/workbench/config/skills.ts

类型定义对应文件是:

text 复制代码
src/features/workbench/types.ts

整体思路是:

text 复制代码
skills.ts              维护完整技能配置
ChatComposer.vue       展示技能市场和已选技能
ComposerSubmitPayload  定义发送给业务层的数据结构
WorkbenchPage.vue      接收 payload 并模拟 AI 回复

定义技能类型

先在 types.ts 中定义技能 key:

ts 复制代码
export type AgentSkillKey =
  | 'generate-page'
  | 'optimize-style'
  | 'split-component'
  | 'fix-issue'
  | 'write-doc';

这些 key 是给程序识别用的。

比如:

text 复制代码
generate-page    生成网页
optimize-style   优化样式
split-component  拆分组件
fix-issue        修复问题
write-doc        生成文档

然后定义技能分类:

ts 复制代码
export type AgentSkillCategory = 'generation' | 'optimization' | 'engineering' | 'document';

最后定义完整的技能结构:

ts 复制代码
export interface AgentSkill {
  key: AgentSkillKey;
  name: string;
  description: string;
  category: AgentSkillCategory;
  enabled: boolean;
  icon: Component;
  prompt: string;
  placeholder: string;
  tags?: string[];
  defaultSelected?: boolean;
}

这样技能就不是一个简单 label,而是一份完整配置。

定义提交数据

为了让发送更接近真实业务,我没有继续只提交字符串。

而是新增了:

ts 复制代码
export interface ComposerSubmitPayload {
  message: string;
  skillKeys: AgentSkillKey[];
  skills: Pick<AgentSkill, 'key' | 'name' | 'category' | 'prompt'>[];
}

也就是说,用户点击发送后,业务层拿到的是:

ts 复制代码
{
  message: '帮我生成一个科技企业官网',
  skillKeys: ['generate-page', 'optimize-style'],
  skills: [
    {
      key: 'generate-page',
      name: '生成网页',
      category: 'generation',
      prompt: '你是一个前端页面生成助手...'
    }
  ]
}

这里最关键的是:

text 复制代码
message 是用户输入
skillKeys 是技能标识
skills 里带了技能的 prompt 和分类信息

后续如果要接真实后端,就可以把这些信息作为请求参数传过去。

抽离技能配置

技能配置放在:

text 复制代码
src/features/workbench/config/skills.ts

示例:

ts 复制代码
export const agentSkills: AgentSkill[] = [
  {
    key: 'generate-page',
    name: '生成网页',
    description: '根据自然语言需求生成页面结构和初版布局',
    category: 'generation',
    enabled: true,
    icon: AppstoreOutlined,
    prompt: '你是一个前端页面生成助手,请根据用户需求生成清晰的页面结构、模块划分和初版布局方案。',
    placeholder: '描述你想生成的网页,例如:帮我生成一个科技企业官网首页',
    tags: ['page', 'layout', 'zero-code'],
    defaultSelected: true,
  },
];

这样做有几个好处:

  • 技能配置集中管理;
  • UI 组件不再写死技能数据;
  • 后续可以从接口加载技能;
  • 可以做权限控制;
  • 可以根据技能切换 placeholder;
  • 可以把 skill key 传给后端做 Agent 调度。

ChatComposer 中怎么使用

ChatComposer.vue 中引入技能配置:

ts 复制代码
import { agentSkills, defaultAgentSkillKeys } from '../../config/skills';
import type { AgentSkillKey, ComposerSubmitPayload } from '../../types';

然后定义当前选中的技能:

ts 复制代码
const selectedSkillKeys = ref<AgentSkillKey[]>([...defaultAgentSkillKeys]);

这里默认选中配置里带有:

ts 复制代码
defaultSelected: true

的技能。

当前可用技能这样计算:

ts 复制代码
const skillOptions = computed(() => agentSkills.filter(skill => skill.enabled));

意思是:

只展示 enabled 为 true 的技能。

当前已选技能这样计算:

ts 复制代码
const selectedSkills = computed(() => {
  return skillOptions.value.filter(skill => selectedSkillKeys.value.includes(skill.key));
});

意思是:

从全部可用技能里筛选出当前已经选中的技能。

技能市场下拉怎么实现

技能市场放在 Senderfooter 插槽里。

因为当前 Vue 版 Sender 支持:

vue 复制代码
<template #footer>
  自定义底部内容
</template>

所以我在 footer 里放了:

vue 复制代码
<a-dropdown :trigger="['click']">
  <button type="button" class="composer-tool-button">
    <ProjectOutlined />
    <span>技能市场</span>
    <DownOutlined />
  </button>

  <template #overlay>
    <div class="skill-menu">
      ...
    </div>
  </template>
</a-dropdown>

这段逻辑就是:

text 复制代码
点击"技能市场"
  ↓
展开技能列表
  ↓
点击某个技能
  ↓
选中或取消选中

其中多选逻辑是:

ts 复制代码
const toggleSkill = (skillKey: AgentSkillKey) => {
  if (isSkillSelected(skillKey)) {
    selectedSkillKeys.value = selectedSkillKeys.value.filter(key => key !== skillKey);
    return;
  }

  selectedSkillKeys.value = [...selectedSkillKeys.value, skillKey];
};

这段代码的意思是:

text 复制代码
如果技能已经选中,再点击就取消
如果技能还没选中,点击就加入

已选技能怎么展示

已选技能放在 Senderprefix 插槽里。

代码大致是:

vue 复制代码
<template #prefix>
  <div v-if="selectedSkills.length" class="skill-chip-list">
    <button
      v-for="skill in selectedSkills"
      :key="skill.key"
      type="button"
      class="skill-chip"
    >
      <component :is="skill.icon" />
      <span>{{ skill.name }}</span>
      <CloseOutlined @click.stop="removeSkill(skill.key)" />
    </button>
  </div>
</template>

这段就是输入框左上角的技能标签。

比如当前选中了:

text 复制代码
生成网页
优化样式

输入框里就会显示:

点击 × 会执行:

ts 复制代码
const removeSkill = (skillKey: AgentSkillKey) => {
  selectedSkillKeys.value = selectedSkillKeys.value.filter(key => key !== skillKey);
};

也就是把这个技能从已选列表里移除。

placeholder 怎么跟着技能变化

为了让输入框更像真实 Agent 产品,我让 placeholder 根据当前技能变化。

ts 复制代码
const placeholder = computed(() => {
  if (selectedSkills.value.length === 0) {
    return '先选择技能,再描述你想让 AI 完成的任务';
  }

  if (selectedSkills.value.length === 1) {
    return selectedSkills.value[0].placeholder;
  }

  return `描述需求,当前技能:${selectedSkills.value.map(skill => skill.name).join('、')}`;
});

逻辑是:

text 复制代码
没有选择技能:提示先选择技能
只选择一个技能:使用该技能自己的 placeholder
选择多个技能:展示当前选择了哪些技能

这样用户输入前就能知道:

text 复制代码
当前 AI 会按哪些技能处理我的需求

发送时如何带上技能

这是这次改造里最重要的一步。

发送函数是:

ts 复制代码
const handleSubmit = (value: string) => {
  const text = value.trim();
  if (!text || props.running) return;

  emit('submit', {
    message: text,
    skillKeys: [...selectedSkillKeys.value],
    skills: selectedSkills.value.map(skill => ({
      key: skill.key,
      name: skill.name,
      category: skill.category,
      prompt: skill.prompt,
    })),
  });

  inputValue.value = '';
};

这里先做了两个判断:

ts 复制代码
if (!text || props.running) return;

意思是:

text 复制代码
如果输入为空,不发送
如果正在生成中,不重复发送

然后提交:

ts 复制代码
message
skillKeys
skills

这一步让技能真正参与了业务流程。

如果只是显示技能标签,但发送时不带技能,那它只是 UI。

现在发送时带上技能,后续就可以根据技能做:

  • 不同 prompt 拼接;
  • 不同任务流程;
  • 不同执行过程展示;
  • 不同结果格式处理。

父组件怎么接收

复制代码
父组件的接受会在后续流程中介绍(TOdolist)

当前实现的价值

这次 ChatComposer 改造主要完成了几件事:

  • 使用 Sender 搭建 Agent 输入框;
  • 通过 footer 插槽实现技能市场入口;
  • 通过 prefix 插槽展示已选技能;
  • 支持多个技能同时选择;
  • 支持移除已选技能;
  • 技能配置从组件中抽离;
  • 定义完整的 AgentSkill 类型;
  • 定义 ComposerSubmitPayload
  • 发送消息时携带技能信息;
  • 为后续接入真实 Agent 服务预留数据结构。

所以它不是单纯做了一个下拉菜单。

更准确地说,它完成的是:

基于现有 Sender 能力,在业务层实现一个可用于生产扩展的 Agent 技能输入模型。

总结

这次改造的核心目标是让 ChatComposer 更像真实 AI Agent 产品里的输入区。

在 AI 时代,输入框不只是输入文本的地方,它还需要承载任务意图、技能选择、附件、上下文等信息。

React 版 Ant Design X 的 Sender 已经提供了 skillslotConfig,可以直接支持技能和词槽模式。

但当前 ant-design-x-vue@1.6.0 还没有这套 API。

所以我在业务项目里选择了一种更稳的方式:

text 复制代码
UI 上使用 Sender 的 prefix/footer 插槽
数据上设计完整 AgentSkill 配置
提交上使用 ComposerSubmitPayload 传递 message + skills

这样既能保持现有组件库不动,又能让业务输入框具备生产环境所需要的技能上下文。

后续如果 Vue 版组件库补齐原生 skill 能力,也可以把现在这套业务数据模型平滑迁移过去。

参考资料

相关推荐
用户45989204565161 小时前
一套 KMP 多仓库版本治理工作流:用 Version Catalog + CI 把发版流水线自动化
前端·kotlin
小赵同学WoW1 小时前
1-4 TypeScript 中的 `number`、`bigint` 与 `string`
前端
小赵同学WoW1 小时前
1-3 `boolean` 与字面量类型
前端
光影少年1 小时前
react navite 长列表优化:FlatList / SectionList 原理与优化属性
前端·react native·react.js
cindershade1 小时前
鼠标 3D 倾斜卡片
前端
wordbaby1 小时前
从双端内卷到一库多端:基于 pnpm + Taro 4 + RNOH 鸿蒙的 Monorepo 跨端架构实战
前端·微信小程序·app
子兮曰1 小时前
DeepSeek Harness 架构深潜:一个把 Agent 运行时做成纯插件树的开源 Harness
前端·后端·deepseek
子兮曰2 小时前
AI Agent 完整入门指南:从 LLM 到生产落地的 30+ 个核心概念
前端·后端·agent
楚宝ovo2 小时前
新增一个 demo 子应用 - 微前端
前端