与本地ollama模型对话
本章我们将完善对话界面,将webview端选中的模型和输入的对话消息发送至extension host端。然后在extension host端调用ollama API与模型进行对话,获取模型返回消息,解析后写入文件。
定义新的proto
首先我们将定义一个message.proto,用来描述要发送到extension host端的消息,该模型也用来在webview上显示发送和接收到的消息。
proto/minicline/message.proto
syntax = "proto3";
package minicline;
message Message {
string content = 1;
MessageOwn owner = 2;
}
enum MessageOwn {
SYSTEM = 0;
ASSISTANT = 1;
USER = 2;
}
然后我们定义一个task.proto,其中的newTask调用用来将我们收集到的模型名字和对话消息发送给extension host端。
proto/minicline/task.proto
syntax = "proto3";
package minicline;
import "minicline/common.proto";
service TaskService {
rpc newTask(NewTaskRequest) returns (String);
}
message NewTaskRequest {
string model = 1;
string text = 2;
}
同样,我们需要导出newTask函数来作为TaskService.newTask的处理函数。这里我们先简单的打印收到model和text信息,然后返回一个虚假的taskID.
src/core/controller/task/newTask.ts
import { NewTaskRequest } from "@/shared/proto/minicline/task";
import { Controller } from "..";
import { String } from "@shared/proto/minicline/common";
export async function newTask(controller: Controller, request: NewTaskRequest): Promise<String> {
console.log(request);
const taskId = "fakeTaskId";
return String.create({ value: taskId || "" });
}
运行npm run proto编译。
更新webview界面
让我们在webview的中间区域添加消息列表,用来显示我们发送的消息和AI助手返回的消息。
webview-ui/src/App.tsx
import { useState } from "react";
...
function App() {
const [selectedModel, setSelectedModel] = useState("");
const initMessages: Message[] = [Message.create({
content: "你好,我是你的AI编程助手MiniCline",
owner:MessageOwn.ASSISTANT
})];
const [messages, setMessages] = useState(initMessages);
return (
<div className="container">
<div className="top">
<ModelBar selectedModel={selectedModel} selectModel={setSelectedModel}/>
</div>
<div className="center">
<Messages messages={messages} />
</div>
<div className="bottom">
<Chatbox
selectedModel={selectedModel}
messages={messages}
setMessages={setMessages}/>
</div>
</div>
);
}
...
我们在主程序初始化选中的模型和消息列表的状态,然后传递给其他组件共享状态。
webview-ui/src/components/message/message.tsx
...
function Messages({ messages }: { messages: Message[] }) {
return (
<>
{messages?.map((message: Message) => (
<div className={message.owner === MessageOwn.USER ? "message_user" : "message"}>
<div className="texts">
<p>{message.content}</p>
</div>
</div>
))}
</>
);
}
...
webview-ui/src/components/models/models.tsx
...
function ModelBar({ selectedModel, selectModel }: { selectedModel: string, selectModel: Function }) {
...
const handleListModelsMessage = useCallback(
async () => {
const result: StringArray = await OllamaServiceClient.listModels(EmptyRequest.create());
...
selectModel(result.values[0]);
}, [models]
);
return (
...
<VSCodeDropdown id="models" className="modellist" onChange={(e: any) => selectModel(e.target._value)}>
...
);
}
...
webview-ui/src/components/chat/chat.tsx
...
function Chatbox({ selectedModel, messages, setMessages }: { selectedModel: string, messages: Message[], setMessages: Function }) {
const [taskId, setTaskId] = useState("");
const [content, setContent] = useState("");
const handleNewTaskMessage = useCallback(
async (model: string, text: string) => {
const taskId: String = await TaskServiceClient.newTask(NewTaskRequest.create({
model: model,
text: text
}));
setTaskId(taskId.value);
setMessages([...messages, Message.create({
content: text,
owner: MessageOwn.USER
})]);
setContent("");
}, [taskId, messages]
);
const onSend = () => {
handleNewTaskMessage(selectedModel, content);
};
return (
<>
<div className="chat-box">
<div>
<VSCodeTextArea className="chatinput"
placeholder="say hello"
value={content}
onChange={(e: any) => setContent(e.target._value)}>
</VSCodeTextArea>
</div>
<div className="button-container">
<div>
<span className="codicon codicon-send" onClick={onSend}></span>
</div>
</div>
</div>
</>
);
}
...
在chatbox中我们将输入的文本和当前选中的模型发送给extension host,记录返回的taskID,然后将消息添加到消息列表中并且清空输入框。
运行npm run build:webview重新编译react应用,启动插件项目,确保本地ollama已经启动,输入一些消息点击发送图标,如果一切正常如下图所示。
添加新的第三方库用于与Ollama对话
package.json
{
"devDependencies": {
...
"@types/clone-deep": "^4.0.4",
...
},
"dependencies": {
...
"@anthropic-ai/sdk": "^0.37.0",
"ollama": "^0.5.13",
"clone-deep": "^4.0.1",
...
}
}
我们引入了@anthropic-ai包里边定义好的数据结构,同时引入ollama库用于与本地服务对话。clone-deep用来深度拷贝流式处理过程中的数据。运行npm run install:all安装依赖.
与本地Ollama对话
我们首先在controller类中添加一个initTask方法,在newTask接收到webview的模型和对话内容后调用该方法开始一个新的任务。开始新任务的代码如下。
src/core/controller/task/index.ts
...
export class Task {
...
constructor(...) {
...
this.api = new OllamaHandler(this.model);
};
public async startTask(): Promise<void> {
const processedUserContent: Anthropic.TextBlockParam[] = [
{
type: "text",
text: `<task>\n${this.task}\n</task>`,
}
];
processedUserContent.push({
type: "text",
text: FocusChainPrompts.recommended,
});
const stream = this.attemptApiRequest(processedUserContent);
try {
for await (const chunk of stream) {
switch (chunk.type) {
...
case "text": {
assistantMessage += chunk.text;
this.assistantMessageContent = parseAssistantMessageV2(assistantMessage);
this.presentAssistantMessage();
break;
}
}
}
} catch (error) {
console.log(error);
}
...
}
async *attemptApiRequest(processedUserContent: Anthropic.TextBlockParam[]): ApiStream {
const systemPrompt = await getSystemPrompt();
const userMessages: Anthropic.MessageParam[] = [{
role: "user",
content: processedUserContent
}];
// Response API requires native tool calls to be enabled
const stream = this.api.createMessage(systemPrompt, userMessages);
const iterator = stream[Symbol.asyncIterator]();
try {
const firstChunk = await iterator.next();
yield firstChunk.value;
} catch (error) {
console.log(error);
}
yield* iterator;
}
async presentAssistantMessage() {
const block = cloneDeep(this.assistantMessageContent[this.currentStreamingContentIndex]);
if (block) {
switch (block.type) {
...
case "tool_use":
await this.executeTool(block);
break;
}
...
}
}
public async executeTool(block: ToolUse): Promise<void> {
if (block.params.path && block.params.content) {
const filePath = path.join(this.cwd, block.params.path);
// 将内容写入文件
await fs.writeFile(filePath, block.params.content, 'utf-8');
}
}
}
我们在startTask调用attemptApiRequest与本地Ollama对话,使用准备好的系统提示词和用户提示词规范模型返回输出。然后循环读取对话输出流的内容,使用parseAssistantMessageV2解析特定内容。当匹配到write_to_file标签时调用本地工具写入文件。写到这里我们可以知道类似cline之类的本地agent是十分依赖于模型的输出内容的。当模型正确按照我们的系统提示词输出内容时,返回差不多如下。
<write_to_file>
<path>hello.c</path>
<content>#include <stdio.h>
int main() {
printf("Hello, World!\\n");
return 0;
}
</content>
<task_progress>
- [x] 创建C语言Hello World程序文件
- [x] 编写包含标准输入输出库的代码
- [x] 实现main函数
- [x] 添加打印"Hello, World!"语句
- [x] 返回0表示程序成功结束
</task_progress>
</write_to_file>
这时候我们会解析<write_to_file>中的内容,从path获取文件名,从content获取文件内容,然后写入workspace下的该文件。
使用cline的示例测试
根据cline官网的例子Build Your First Project,我们在vscode中打开一个本地目录,比如c:/git/testvibe, 在我webview中选择qwen3-coder:latest,在对话框输入如下内容,点击发送按钮。
Create a simple website in a single HTML file. It should have:
- A welcome message saying "Hello from Cline!"
- A colorful gradient background
- A button that cycles through different color themes when clicked
- Modern, clean design
- All CSS and JavaScript should be included in the same HTML file
如果一切运行正常(在我本机差不多运行了2分多种),会在workspace目录下生成index.html,预览效果如下,点击按钮可以切换主题。
总结
系列入门教程到此告一段落。