给 tool use 设计负反馈机制

This commit is contained in:
锦恢 2025-04-24 20:51:55 +08:00
parent dd9c117df7
commit f14687b1d8
5 changed files with 262 additions and 56 deletions

View File

@ -3,8 +3,9 @@
<el-scrollbar ref="scrollbarRef" :height="'90%'" @scroll="handleScroll" v-if="renderMessages.length > 0 || isLoading"> <el-scrollbar ref="scrollbarRef" :height="'90%'" @scroll="handleScroll" v-if="renderMessages.length > 0 || isLoading">
<div class="message-list" :ref="el => messageListRef = el"> <div class="message-list" :ref="el => messageListRef = el">
<div v-for="(message, index) in renderMessages" :key="index" <div v-for="(message, index) in renderMessages" :key="index"
:class="['message-item', message.role.split('/')[0]]"> :class="['message-item', message.role.split('/')[0]]"
<div class="message-avatar" v-if="message.role.split('/')[0] === 'assistant'"> >
<div class="message-avatar" v-if="message.role.startsWith('assistant')">
<span class="iconfont icon-robot"></span> <span class="iconfont icon-robot"></span>
</div> </div>
@ -131,6 +132,7 @@ interface IRenderMessage {
tool_calls?: ToolCall[]; tool_calls?: ToolCall[];
showJson?: Ref<boolean>; showJson?: Ref<boolean>;
extraInfo: IExtraInfo; extraInfo: IExtraInfo;
isLast: boolean;
} }
const renderMessages = computed(() => { const renderMessages = computed(() => {
@ -140,7 +142,8 @@ const renderMessages = computed(() => {
messages.push({ messages.push({
role: 'user', role: 'user',
content: message.content, content: message.content,
extraInfo: message.extraInfo extraInfo: message.extraInfo,
isLast: false
}); });
} else if (message.role === 'assistant') { } else if (message.role === 'assistant') {
if (message.tool_calls) { if (message.tool_calls) {
@ -149,13 +152,15 @@ const renderMessages = computed(() => {
content: message.content, content: message.content,
tool_calls: message.tool_calls, tool_calls: message.tool_calls,
showJson: ref(false), showJson: ref(false),
extraInfo: message.extraInfo extraInfo: message.extraInfo,
isLast: false
}); });
} else { } else {
messages.push({ messages.push({
role: 'assistant/content', role: 'assistant/content',
content: message.content, content: message.content,
extraInfo: message.extraInfo extraInfo: message.extraInfo,
isLast: false
}); });
} }
@ -169,6 +174,11 @@ const renderMessages = computed(() => {
} }
} }
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1];
lastMessage.isLast = true;
}
return messages; return messages;
}); });
@ -375,11 +385,6 @@ onUnmounted(() => {
line-height: 1.6; line-height: 1.6;
} }
.message-text.tool_calls {
border-left: 3px solid var(--main-color);
padding-left: 10px;
}
.user .message-text { .user .message-text {
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;

View File

@ -1,29 +1,34 @@
<template> <template>
<div class="message-role"> <div class="message-role">
Agent Agent
<span class="message-reminder" v-if="!props.message.toolResult"> <span class="message-reminder" v-if="props.message.isLast && !props.message.toolResult">
正在使用工具 正在使用工具
<span class="tool-loading iconfont icon-double-loading"> <span class="tool-loading iconfont icon-double-loading">
</span> </span>
</span> </span>
</div> </div>
<div class="message-text tool_calls"> <div class="message-text tool_calls" :class="{ 'fail': !props.message.isLast && !props.message.toolResult }">
<div v-if="props.message.content" v-html="markdownToHtml(props.message.content)"></div> <div v-if="props.message.content" v-html="markdownToHtml(props.message.content)"></div>
<el-collapse v-model="activeNames">
<el-collapse-item name="tool">
<template #title>
<div class="tool-calls"> <div class="tool-calls">
<div v-for="(call, index) in props.message.tool_calls" :key="index" class="tool-call-item">
<div class="tool-call-header"> <div class="tool-call-header">
<span class="tool-type">{{ 'tool' }}</span> <span class="tool-type">{{ 'tool use' }}</span>
<span class="tool-name">{{ call.function.name }}</span> <span class="tool-name">{{ props.message.tool_calls[0].function.name }}</span>
<el-button size="small" @click="createTest(call)"> <el-button size="small" @click="createTest(props.message.tool_calls[0])">
<span class="iconfont icon-send"></span> <span class="iconfont icon-send"></span>
</el-button> </el-button>
</div> </div>
</div>
</template>
<div>
<div class="tool-arguments"> <div class="tool-arguments">
<div class="inner"> <div class="inner">
<div v-html="jsonResultToHtml(call.function.arguments)"></div> <div v-html="jsonResultToHtml(props.message.tool_calls[0].function.arguments)"></div>
</div>
</div>
</div> </div>
</div> </div>
@ -32,8 +37,8 @@
<div class="tool-call-header"> <div class="tool-call-header">
<span class="tool-name">{{ "响应" }}</span> <span class="tool-name">{{ "响应" }}</span>
<span style="width: 200px;" class="tools-dialog-container"> <span style="width: 200px;" class="tools-dialog-container">
<el-switch v-model="props.message.showJson!.value" inline-prompt active-text="JSON" inactive-text="Text" <el-switch v-model="props.message.showJson!.value" inline-prompt active-text="JSON"
style="margin-left: 10px; width: 200px;" inactive-text="Text" style="margin-left: 10px; width: 200px;"
:inactive-action-style="'backgroundColor: var(--sidebar)'" /> :inactive-action-style="'backgroundColor: var(--sidebar)'" />
</span> </span>
</div> </div>
@ -54,11 +59,14 @@
</div> </div>
</div> </div>
</div> </div>
</el-collapse-item>
</el-collapse>
</div>
<MessageMeta :message="message" /> <MessageMeta :message="message" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps } from 'vue'; import { defineProps, ref, watch } from 'vue';
import MessageMeta from './message-meta.vue'; import MessageMeta from './message-meta.vue';
import { markdownToHtml } from '../markdown'; import { markdownToHtml } from '../markdown';
@ -71,6 +79,19 @@ const props = defineProps({
} }
}); });
const activeNames = ref<string[]>(props.message.toolResult ? [''] : ['tool']);
watch(
() => props.message.toolResult,
(value, oldValue) => {
if (value) {
setTimeout(() => {
activeNames.value = [''];
}, 1000);
}
}
);
const jsonResultToHtml = (jsonString: string) => { const jsonResultToHtml = (jsonString: string) => {
const formattedJson = JSON.stringify(JSON.parse(jsonString), null, 2); const formattedJson = JSON.stringify(JSON.parse(jsonString), null, 2);
const html = markdownToHtml('```json\n' + formattedJson + '\n```'); const html = markdownToHtml('```json\n' + formattedJson + '\n```');
@ -89,6 +110,14 @@ const isValidJSON = (str: string) => {
</script> </script>
<style> <style>
.message-text.tool_calls {
border-left: 3px solid var(--main-color);
padding-left: 10px;
}
.message-text.tool_calls.fail {
border-left: 3px solid var(--el-color-error);
}
.tool-calls { .tool-calls {
margin-top: 10px; margin-top: 10px;
@ -123,6 +152,7 @@ const isValidJSON = (str: string) => {
align-items: center; align-items: center;
border-radius: 4px; border-radius: 4px;
margin-right: 10px; margin-right: 10px;
height: 22px;
} }
.tool-arguments { .tool-arguments {
@ -151,6 +181,4 @@ const isValidJSON = (str: string) => {
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
margin-top: 4px; margin-top: 4px;
} }
</style> </style>

View File

@ -18,7 +18,7 @@ export function createTest(call: ToolCall) {
tab.component = markRaw(debugModes[2]); tab.component = markRaw(debugModes[2]);
tab.icon = 'icon-tool'; tab.icon = 'icon-tool';
tab.name = t("tools"); tab.name = t("tools");
1
const storage: ToolStorage = { const storage: ToolStorage = {
currentToolName: call.function.name, currentToolName: call.function.name,
formData: JSON.parse(call.function.arguments) formData: JSON.parse(call.function.arguments)

View File

@ -0,0 +1,129 @@
{
"currentIndex": 1,
"tabs": [
{
"name": "空白测试 2",
"icon": "icon-blank",
"type": "blank",
"componentIndex": -1,
"storage": {}
},
{
"name": "交互测试",
"icon": "icon-robot",
"type": "blank",
"componentIndex": 3,
"storage": {
"messages": [
{
"role": "user",
"content": "今天天气",
"extraInfo": {
"created": 1745498953049,
"serverName": "Huoshan DeepSeek"
}
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_zw5axi9gftk294vifdlfv83y",
"index": 0,
"type": "function",
"function": {
"name": "puppeteer_navigate",
"arguments": "{\"url\":\"https://www.weather.com\"}"
}
}
],
"extraInfo": {
"created": 1745498953891,
"serverName": "Huoshan DeepSeek"
}
},
{
"role": "tool",
"tool_call_id": "call_zw5axi9gftk294vifdlfv83y",
"content": "[{\"type\":\"text\",\"text\":\"Navigated to https://www.weather.com\"}]",
"extraInfo": {
"created": 1745498965901,
"serverName": "Huoshan DeepSeek"
}
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_enwp8eh9q0rklmz1cr0lfinx",
"index": 0,
"type": "function",
"function": {
"name": "puppeteer_evaluate",
"arguments": "{\"script\":\"document.querySelector('.CurrentConditions--tempValue--3KcTQ').innerText\"}"
}
}
],
"extraInfo": {
"created": 1745498967051,
"serverName": "Huoshan DeepSeek"
}
},
{
"role": "assistant",
"content": "错误: 工具调用失败: [object Object]",
"extraInfo": {
"created": 1745498967057,
"serverName": "Huoshan DeepSeek"
}
}
],
"settings": {
"modelIndex": 8,
"enableTools": [
{
"name": "puppeteer_navigate",
"description": "Navigate to a URL",
"enabled": true
},
{
"name": "puppeteer_screenshot",
"description": "Take a screenshot of the current page or a specific element",
"enabled": true
},
{
"name": "puppeteer_click",
"description": "Click an element on the page",
"enabled": true
},
{
"name": "puppeteer_fill",
"description": "Fill out an input field",
"enabled": true
},
{
"name": "puppeteer_select",
"description": "Select an element on the page with Select tag",
"enabled": true
},
{
"name": "puppeteer_hover",
"description": "Hover an element on the page",
"enabled": true
},
{
"name": "puppeteer_evaluate",
"description": "Execute JavaScript in the browser console",
"enabled": true
}
],
"enableWebSearch": false,
"temperature": 0.7,
"contextLength": 10,
"systemPrompt": ""
}
}
}
]
}

View File

@ -81,6 +81,50 @@
"prompt_cache_miss_tokens": 84 "prompt_cache_miss_tokens": 84
} }
} }
},
{
"role": "user",
"content": "再试一次",
"extraInfo": {
"created": 1745495349299,
"serverName": "Huoshan DeepSeek"
}
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_jpm4v1e92v4nq650wlqkhm56",
"index": 0,
"type": "function",
"function": {
"name": "get_weather_by_city_code",
"arguments": "{\"city_code\":101210101}"
}
}
],
"extraInfo": {
"created": 1745495350097,
"serverName": "Huoshan DeepSeek"
}
},
{
"role": "tool",
"tool_call_id": "call_jpm4v1e92v4nq650wlqkhm56",
"content": "[{\"type\":\"text\",\"text\":\"CityWeather(city_name_en='hangzhou', city_name_cn='杭州', city_code='101210101', temp='18.3', wd='', ws='', sd='89%', aqi='57', weather='阴')\"}]",
"extraInfo": {
"created": 1745495350319,
"serverName": "Huoshan DeepSeek"
}
},
{
"role": "assistant",
"content": "杭州的最新天气信息如下:\n\n- 城市:杭州\n- 温度18.3°C\n- 天气状况:阴\n- 湿度89%\n- 空气质量指数 (AQI)57良好\n\n天气较为凉爽适合外出活动",
"extraInfo": {
"created": 1745495352693,
"serverName": "Huoshan DeepSeek"
}
} }
], ],
"settings": { "settings": {