PowerShell 技能连载 - Prompt Engineering 实践

适用于 PowerShell 7.0 及以上版本

随着大语言模型(LLM)的快速普及,Prompt Engineering 已经成为运维工程师不可或缺的技能。在
PowerShell 生态中,我们可以将提示词工程与自动化脚本深度结合,构建出智能化的运维工具链。无论是在
Azure 资源管理、日志分析还是故障诊断场景中,精心设计的 Prompt 都能大幅提升 LLM 输出的准确性和可用性。

传统的运维脚本通常依赖固定逻辑处理已知场景,但面对模糊的故障描述或复杂的架构问题时往往力不从心。
通过 Prompt Engineering,我们可以让 LLM 理解系统上下文,生成符合 PowerShell 风格的命令和脚本,
甚至自动分析错误日志并给出修复建议。关键在于如何用结构化的方式管理与 LLM 的交互。

本文将从模板管理、结构化输出控制和运维 Copilot 实现三个层面,展示如何在 PowerShell 中系统化地
应用 Prompt Engineering 最佳实践。

Prompt 模板管理

在生产环境中,我们通常需要维护多套 Prompt 模板来应对不同的运维场景。手动拼接字符串既容易出错,
也难以维护。下面是一个可复用的 Prompt 模板管理系统,支持变量替换和上下文管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
class PromptTemplate {
[string]$Name
[string]$Template
[hashtable]$Variables = @{}
[string]$SystemContext

PromptTemplate([string]$Name, [string]$Template) {
$this.Name = $Name
$this.Template = $Template
}

[void] SetVariable([string]$Key, [object]$Value) {
$this.Variables[$Key] = $Value
}

[void] SetSystemContext([string]$Context) {
$this.SystemContext = $Context
}

[string] Render() {
$rendered = $this.Template
foreach ($key in $this.Variables.Keys) {
$placeholder = "{{$key}}"
$rendered = $rendered -replace [regex]::Escape($placeholder), $this.Variables[$key]
}
return $rendered
}

[hashtable] ToChatMessages() {
$messages = @()
if ($this.SystemContext) {
$messages += @{
role = 'system'
content = $this.SystemContext
}
}
$messages += @{
role = 'user'
content = $this.Render()
}
return @{ messages = $messages }
}
}

# 创建运维场景的 Prompt 模板库
$templates = @{}

$templates['error-diagnosis'] = [PromptTemplate]::new(
'error-diagnosis',
'请分析以下 PowerShell 错误信息,给出可能的原因和修复建议。' +
"`n`n错误类型:{ErrorType}" +
"`n错误消息:{ErrorMessage}" +
'`n发生环境:{Environment}'
)

$templates['script-generation'] = [PromptTemplate]::new(
'script-generation',
'请生成一个 PowerShell 脚本,要求如下:' +
"`n目标:{Goal}" +
"`n约束条件:{Constraints}" +
"`n目标平台:{Platform}" +
'`n请使用 PowerShell 7 兼容语法,包含错误处理和注释。'
)

# 设置系统上下文:定义 LLM 的角色和行为规范
$systemContext = @"
你是一位资深的 PowerShell 和 Azure 运维专家。
你的回答需要:
1. 使用 PowerShell 7 兼容语法
2. 包含完整的错误处理(try/catch/finally)
3. 遵循最佳实践(Approved Verbs、强类型、注释)
4. 输出可直接执行的脚本代码
"@

$templates['error-diagnosis'].SetSystemContext($systemContext)
$templates['script-generation'].SetSystemContext($systemContext)

# 使用模板:填充变量并渲染
$tpl = $templates['error-diagnosis']
$tpl.SetVariable('ErrorType', 'InvalidOperationException')
$tpl.SetVariable('ErrorMessage', 'Collection was modified; enumeration operation may not execute.')
$tpl.SetVariable('Environment', 'Windows Server 2022, PowerShell 7.4')

$renderedPrompt = $tpl.Render()
$chatPayload = $tpl.ToChatMessages()

Write-Host "=== 渲染后的 Prompt ===" -ForegroundColor Cyan
Write-Host $renderedPrompt
Write-Host "`n=== Chat API 载荷 ===" -ForegroundColor Cyan
$chatPayload | ConvertTo-Json -Depth 3

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
=== 渲染后的 Prompt ===
请分析以下 PowerShell 错误信息,给出可能的原因和修复建议。

错误类型:InvalidOperationException
错误消息:Collection was modified; enumeration operation may not execute.
发生环境:Windows Server 2022, PowerShell 7.4

=== Chat API 载荷 ===
{
"messages": [
{
"role": "system",
"content": "你是一位资深的 PowerShell 和 Azure 运维专家。\n你的回答需要:\n1. 使用 PowerShell 7 兼容语法\n2. 包含完整的错误处理(try/catch/finally)\n3. 遵循最佳实践(Approved Verbs、强类型、注释)\n4. 输出可直接执行的脚本代码\n"
},
{
"role": "user",
"content": "请分析以下 PowerShell 错误信息,给出可能的原因和修复建议。\n\n错误类型:InvalidOperationException\n错误消息:Collection was modified; enumeration operation may not execute.\n发生环境:Windows Server 2022, PowerShell 7.4"
}
]
}

结构化输出控制

LLM 返回自由文本虽然灵活,但在自动化流程中很难直接使用。通过精心设计 Prompt,我们可以
引导 LLM 输出结构化的 JSON,再由 PowerShell 解析为对象。下面展示如何实现输出控制、
Schema 校验和自动重试机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
function Invoke-StructuredCompletion {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$UserPrompt,

[Parameter(Mandatory)]
[string]$JsonSchema,

[string]$Model = 'gpt-4o-mini',

[int]$MaxRetries = 3,

[string]$Endpoint = 'http://localhost:11434/v1/chat/completions'
)

# 构建系统提示词,强制 JSON 输出并给出 Schema
$systemPrompt = @"
你是一个 PowerShell 运维助手。用户会提出运维相关的问题,你需要给出结构化的回答。

严格要求:
1. 只输出合法的 JSON,不要包含任何其他文字说明
2. 严格遵循以下 JSON Schema:
$JsonSchema
3. 不要输出 Markdown 代码块标记,直接输出 JSON
4. 字符串值使用 UTF-8 编码
"@

$attempt = 0
while ($attempt -lt $MaxRetries) {
$attempt++
Write-Verbose "尝试第 $attempt 次..."

$body = @{
model = $Model
messages = @(
@{ role = 'system'; content = $systemPrompt },
@{ role = 'user'; content = $UserPrompt }
)
temperature = 0.1
} | ConvertTo-Json -Depth 5

try {
$response = Invoke-RestMethod -Uri $Endpoint -Method Post `
-ContentType 'application/json' `
-Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
-ErrorAction Stop

$content = $response.choices[0].message.content.Trim()

# 去除可能的 Markdown 代码块标记
if ($content -match '^\x60{3}json?\s*([\s\S]*?)\x60{3}$') {
$content = $Matches[1].Trim()
}

# 尝试解析 JSON
$parsed = $content | ConvertFrom-Json -ErrorAction Stop

Write-Verbose "JSON 解析成功"
return [PSCustomObject]@{
Success = $true
Data = $parsed
Raw = $content
Attempts = $attempt
}
}
catch {
Write-Warning "第 $attempt 次尝试失败:$($_.Exception.Message)"
if ($attempt -ge $MaxRetries) {
return [PSCustomObject]@{
Success = $false
Error = $_.Exception.Message
Attempts = $attempt
}
}
Start-Sleep -Milliseconds ($attempt * 500)
}
}
}

# 定义输出 Schema:服务器健康检查结果
$schema = @'
{
"type": "object",
"properties": {
"server_name": { "type": "string" },
"overall_status": { "type": "string", "enum": ["healthy", "warning", "critical"] },
"checks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"item": { "type": "string" },
"status": { "type": "string" },
"detail": { "type": "string" },
"recommendation": { "type": "string" }
},
"required": ["item", "status", "detail", "recommendation"]
}
},
"summary": { "type": "string" }
},
"required": ["server_name", "overall_status", "checks", "summary"]
}
'@

$result = Invoke-StructuredCompletion -UserPrompt @'
WebSrv01 服务器状态:CPU 使用率 92%,内存剩余 2GB/32GB,
磁盘 C: 剩余 15%,事件日志中发现 37 个 Error 级别事件(过去 1 小时),
IIS 应用池 MyAppPool 已崩溃 3 次。
'@ -JsonSchema $schema -Verbose

if ($result.Success) {
Write-Host "服务器:$($result.Data.server_name)" -ForegroundColor Cyan
Write-Host "状态:$($result.Data.overall_status)" -ForegroundColor Yellow
Write-Host "`n检查项:"
$result.Data.checks | Format-Table -Property item, status, detail -Wrap
Write-Host "摘要:$($result.Data.summary)"
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
详细: 尝试第 1 次...
详细: JSON 解析成功
服务器:WebSrv01
状态:critical

检查项:

item status detail
---- ------ ------
CPU 使用率 critical 持续 92%,远超 80% 阈值
内存使用 warning 剩余 2GB,接近耗尽
磁盘空间 warning C: 盘仅剩 15%
事件日志 critical 1 小时内 37 个 Error 事件
IIS 应用池 critical MyAppPool 已崩溃 3 次

摘要:WebSrv01 服务器处于 critical 状态,CPU 和 IIS 应用池问题最为紧急,
建议立即排查 CPU 占用进程并重启应用池。

运维 Copilot 实现

将前面两节的技术整合起来,我们可以构建一个实用的运维 Copilot 工具。它能够自动收集系统信息,
结合上下文回答运维问题,生成 PowerShell 命令,甚至诊断错误。下面是一个完整的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
class OpsCopilot {
[string]$Model
[string]$Endpoint
[hashtable]$Templates
[string]$SystemInfo

OpsCopilot([string]$Endpoint, [string]$Model) {
$this.Endpoint = $Endpoint
$this.Model = $Model
$this.Templates = @{}
$this.SystemInfo = $this.CollectSystemInfo()
$this.RegisterTemplates()
}

[string] CollectSystemInfo() {
$info = @{
hostname = $env:COMPUTERNAME
os = (Get-CimInstance Win32_OperatingSystem).Caption
psVersion = $PSVersionTable.PSVersion.ToString()
dotnet = [System.Environment]::Version.ToString()
cpu = (Get-CimInstance Win32_Processor).Name
totalMemGB = [math]::Round(
(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 1
)
drives = Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' |
ForEach-Object {
@{
drive = $_.DeviceID
freeGB = [math]::Round($_.FreeSpace / 1GB, 1)
totalGB = [math]::Round($_.Size / 1GB, 1)
}
}
}
return ($info | ConvertTo-Json -Depth 3)
}

[void] RegisterTemplates() {
$this.Templates['ask'] = @'
基于以下系统信息回答运维问题。

系统信息:
{SystemInfo}

问题:{Question}

请给出:
1. 问题分析
2. 具体的 PowerShell 命令或脚本
3. 注意事项
'@

$this.Templates['diagnose'] = @'
基于以下系统信息诊断错误。

系统信息:
{SystemInfo}

错误详情:
{ErrorDetail}

请分析:
1. 错误的根本原因
2. 修复步骤(含 PowerShell 命令)
3. 预防措施
'@
}

[string] RenderTemplate([string]$TemplateName, [hashtable]$Vars) {
$rendered = $this.Templates[$TemplateName]
$rendered = $rendered -replace '\{SystemInfo\}', $this.SystemInfo
foreach ($key in $Vars.Keys) {
$rendered = $rendered -replace "\{$key\}", $Vars[$key]
}
return $rendered
}

[string] Chat([string]$UserMessage) {
$systemPrompt = @"
你是一位精通 PowerShell 和 Windows Server 的运维专家。
当前系统环境:
$($this.SystemInfo)

回答要求:
- 优先使用 PowerShell 7 原生命令
- 脚本包含 try/catch 错误处理
- 给出可立即执行的命令
- 用中文回答
"@

$body = @{
model = $this.Model
messages = @(
@{ role = 'system'; content = $systemPrompt },
@{ role = 'user'; content = $UserMessage }
)
temperature = 0.2
} | ConvertTo-Json -Depth 5

$response = Invoke-RestMethod -Uri $this.Endpoint -Method Post `
-ContentType 'application/json' `
-Body ([System.Text.Encoding]::UTF8.GetBytes($body))

return $response.choices[0].message.content
}

[string] Ask([string]$Question) {
$prompt = $this.RenderTemplate('ask', @{
Question = $Question
})
return $this.Chat($prompt)
}

[string] Diagnose([string]$ErrorDetail) {
$prompt = $this.RenderTemplate('diagnose', @{
ErrorDetail = $ErrorDetail
})
return $this.Chat($prompt)
}
}

# 初始化 Copilot(使用本地 Ollama 或远程 API)
$copilot = [OpsCopilot]::new('http://localhost:11434/v1/chat/completions', 'qwen3:8b')

# 示例 1:提出运维问题
Write-Host "=== 运维问答 ===" -ForegroundColor Cyan
$answer = $copilot.Ask('如何查找占用磁盘空间最大的前 10 个文件夹?')
Write-Host $answer

# 示例 2:诊断错误
Write-Host "`n=== 错误诊断 ===" -ForegroundColor Cyan
$diagnosis = $copilot.Diagnose(@"
执行 Get-EventLog -LogName Application -Newest 100 时报错:
"Requested registry access is not allowed"
当前以普通用户身份运行 PowerShell。
"@)
Write-Host $diagnosis

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
=== 运维问答 ===
[问题分析]
在磁盘空间不足时,需要快速定位占用空间最大的目录,以便清理。

[PowerShell 命令]
使用以下命令查找 D: 盘中最大的 10 个文件夹:

Get-ChildItem -Path 'D:\' -Directory -Recurse -ErrorAction SilentlyContinue |
ForEach-Object {
$size = (Get-ChildItem $_.FullName -Recurse -File -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum).Sum
[PSCustomObject]@{
Path = $_.FullName
SizeMB = [math]::Round($size / 1MB, 2)
}
} | Sort-Object SizeMB -Descending | Select-Object -First 10 |
Format-Table -AutoSize

[注意事项]
1. 对整个盘扫描很慢,建议缩小搜索范围
2. 加 -ErrorAction SilentlyContinue 跳过无权限目录
3. 管道中计算大小是内存友好的方式

=== 错误诊断 ===
[根本原因]
Get-EventLog 需要管理员权限才能访问事件日志注册表项。以普通用户运行时,
会被拒绝访问。

[修复步骤]
# 方法 1:以管理员身份运行 PowerShell
Start-Process pwsh -Verb RunAs

# 方法 2(推荐):改用 Get-WinEvent,它支持更精细的权限控制
Get-WinEvent -LogName 'Application' -MaxEvents 100

# 方法 3:仅查看有权限的日志
Get-WinEvent -ListLog '*' | Where-Object { $_.IsLogFullNameValid }

[预防措施]
1. 优先使用 Get-WinEvent 替代已过时的 Get-EventLog
2. 运维脚本中加入权限检查:
if (-not ([Security.Principal.WindowsPrincipal]::new(
[Security.Principal.WindowsIdentity]::GetCurrent()
)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Warning '建议以管理员身份运行此脚本'
}

注意事项

  1. API 端点安全:生产环境中调用 LLM API 时,务必使用 HTTPS 并通过环境变量或
    Azure Key Vault 管理 API 密钥,不要将密钥硬编码在脚本中。可以使用
    ConvertFrom-SecureStringGet-Secret 安全地获取凭据。

  2. Prompt 注入防护:当 Prompt 中包含用户输入或外部数据时,要对内容进行清理,
    避免恶意指令注入。可以用 -replace 去除特殊字符,或对用户输入做白名单过滤。

  3. 输出校验不可省略:即使指定了 JSON Schema,LLM 仍可能输出不符合预期的内容。
    始终在解析前做格式检查,重试机制是生产环境的必要保障。建议设置合理的
    $MaxRetries(通常 3 次足够)。

  4. 上下文长度管理:收集系统信息时注意控制 Token 数量。对于大型环境(数百台服务器),
    先做摘要或筛选再传入 Prompt,避免超出模型的上下文窗口限制。可以用
    ($text | Measure-Object -Character).Characters 估算 Token 消耗。

  5. 模板版本管理:将 Prompt 模板存储为独立的 JSON 或 YAML 文件,纳入 Git 版本控制。
    这样可以追踪模板变更对输出质量的影响,也方便团队成员协作。建议在模板中加入版本号和
    最后更新时间字段。

  6. 本地模型优先:对于包含敏感系统信息的运维场景,优先使用本地部署的模型(如 Ollama、
    vLLM)而非云端 API。这既降低了数据泄露风险,也减少了网络延迟对自动化流程的影响。
    如果必须使用云端 API,确保数据传输经过加密且符合公司的安全合规要求。

PowerShell 技能连载 - 本地 LLM 工具链

适用于 PowerShell 7.0 及以上版本,需要 Ollama 或 LM Studio

背景引入

随着大语言模型(LLM)的快速发展,越来越多的开发者希望在自己的工作站上运行本地模型,以满足数据隐私、离线场景和低延迟的需求。Ollama 和 LM Studio 是目前最流行的两个本地 LLM 运行工具,它们都提供了兼容 OpenAI 格式的 REST API,可以方便地从任何编程语言调用。

PowerShell 作为 Windows 和跨平台运维的核心脚本语言,天然适合充当本地 LLM 的”胶水层”。通过 PowerShell 调用本地模型 API,我们可以将 AI 能力无缝嵌入到日常运维脚本、日志分析、文档处理等任务中,而无需将敏感数据发送到云端。

本文将介绍如何用 PowerShell 构建完整的本地 LLM 工具链,包括模型管理与对话接口、RAG 本地知识库,以及 AI 辅助运维工具三个核心场景。所有代码均基于本地 API,不依赖任何云服务。

Ollama API 集成

Ollama 提供了简洁的 REST API,默认监听 http://localhost:11434。下面的模块封装了模型列表查询、对话生成和流式输出等常用操作,可以作为日常脚本的基础工具集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
function Get-OllamaModel {
<# 获取本地已安装的模型列表 #>
$response = Invoke-RestMethod -Uri 'http://localhost:11434/api/tags' -Method Get
$response.models | ForEach-Object {
[PSCustomObject]@{
Name = $_.name
SizeMB = [math]::Round($_.size / 1MB, 1)
ModifiedAt = $_.modified_at
Family = $_.details.family
Parameter = $_.details.parameter_size
}
} | Sort-Object Name | Format-Table -AutoSize
}

function Invoke-OllamaChat {
<#
.SYNOPSIS
向本地 Ollama 模型发送对话请求
.PARAMETER Model
模型名称,如 qwen2.5:7b、llama3.1:8b
.PARAMETER Messages
消息数组,每条包含 role 和 content
.PARAMETER Stream
是否启用流式输出(默认 $false)
.PARAMETER Temperature
生成温度,0.0 到 1.0 之间
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Model,

[Parameter(Mandatory)]
[hashtable[]]$Messages,

[switch]$Stream,

[ValidateRange(0.0, 1.0)]
[double]$Temperature = 0.7
)

$body = @{
model = $Model
messages = $Messages
stream = $Stream.IsPresent
options = @{
temperature = $Temperature
}
} | ConvertTo-Json -Depth 5

if ($Stream) {
# 流式输出:逐行读取 SSE 响应
$request = [System.Net.Http.HttpRequestMessage]::new(
[System.Net.Http.HttpMethod]::Post,
'http://localhost:11434/api/chat'
)
$request.Content = [System.Net.Http.StringContent]::new(
$body, [System.Text.Encoding]::UTF8, 'application/json'
)

$client = [System.Net.Http.HttpClient]::new()
$response = $client.Send($request, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead)
$reader = [System.IO.StreamReader]::new($response.Content.ReadAsStream())

$fullResponse = [System.Text.StringBuilder]::new()
while (-not $reader.EndOfStream) {
$line = $reader.ReadLine()
if ($line) {
$chunk = $line | ConvertFrom-Json
if ($chunk.message.content) {
Write-Host $chunk.message.content -NoNewline
[void]$fullResponse.Append($chunk.message.content)
}
if ($chunk.done) { break }
}
}
Write-Host ''
$reader.Close()
$client.Dispose()
return $fullResponse.ToString()
}
else {
$result = Invoke-RestMethod -Uri 'http://localhost:11434/api/chat' `
-Method Post -Body $body -ContentType 'application/json'
return $result.message.content
}
}

# 用法示例:查看已安装模型
Get-OllamaModel

# 用法示例:单轮对话
$messages = @(
@{ role = 'system'; content = '你是一个 PowerShell 专家,回答简洁精准。' }
@{ role = 'user'; content = '如何获取系统中占用磁盘最大的 10 个目录?' }
)

$answer = Invoke-OllamaChat -Model 'qwen2.5:7b' -Messages $messages -Temperature 0.3
Write-Host "`n--- 模型回复 ---`n$answer"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Name              SizeMB   ModifiedAt               Family   Parameter
---- ------ ---------- ------ ---------
llama3.1:8b 4661.2 2026-04-18T10:30:00Z llama 8B
qwen2.5:7b 4368.5 2026-04-20T14:22:00Z qwen2 7B
deepseek-r1:7b 4520.8 2026-04-15T09:11:00Z qwen2 7B
nomic-embed-text 274.1 2026-04-10T08:00:00Z nomic 136M

--- 模型回复 ---

可以使用以下 PowerShell 命令获取占用磁盘最大的 10 个目录:

Get-ChildItem -Directory -Recurse -ErrorAction SilentlyContinue |
ForEach-Object {
$size = (Get-ChildItem $_.FullName -Recurse -File -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum).Sum
[PSCustomObject]@{ Path = $_.FullName; SizeMB = [math]::Round($size/1MB, 2) }
} | Sort-Object SizeMB -Descending | Select-Object -First 10

RAG 本地知识库

检索增强生成(RAG)是将本地文档作为上下文注入 LLM 的关键技术。下面的实现包含文档切片、向量化存储和检索查询三个步骤,完全使用本地模型(nomic-embed-text 做嵌入,对话模型做生成),无需外部服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
class LocalVectorStore {
<# 本地向量存储,用于 RAG 检索 #>
[System.Collections.Generic.List[hashtable]]$Documents

LocalVectorStore() {
$this.Documents = [System.Collections.Generic.List[hashtable]]::new()
}

[void] AddChunk([string]$Content, [string]$Source, [double[]]$Embedding) {
$this.Documents.Add(@{
content = $Content
source = $Source
embedding = $Embedding
})
}

[double] CosineSimilarity([double[]]$A, [double[]]$B) {
$dot = 0.0; $normA = 0.0; $normB = 0.0
for ($i = 0; $i -lt $A.Count; $i++) {
$dot += $A[$i] * $B[$i]
$normA += $A[$i] * $A[$i]
$normB += $B[$i] * $B[$i]
}
if ($normA -eq 0 -or $normB -eq 0) { return 0 }
return $dot / ([math]::Sqrt($normA) * [math]::Sqrt($normB))
}

[hashtable[]] Search([double[]]$QueryEmbedding, [int]$TopK = 3) {
return $this.Documents | ForEach-Object {
[PSCustomObject]@{
Content = $_.content
Source = $_.source
Similarity = $this.CosineSimilarity($QueryEmbedding, $_.embedding)
}
} | Sort-Object Similarity -Descending | Select-Object -First $TopK |
ForEach-Object { @{ content = $_.Content; source = $_.Source; score = $_.Similarity } }
}
}

function Get-LocalEmbedding {
param([string]$Text)
$body = @{ model = 'nomic-embed-text'; prompt = $Text } | ConvertTo-Json
$result = Invoke-RestMethod -Uri 'http://localhost:11434/api/embeddings' `
-Method Post -Body $body -ContentType 'application/json'
return $result.embedding
}

function Split-Document {
param(
[string]$Content,
[int]$ChunkSize = 500,
[int]$Overlap = 50
)
$chunks = [System.Collections.Generic.List[string]]::new()
$words = $Content -split '\s+'
$i = 0
while ($i -lt $words.Count) {
$end = [math]::Min($i + $ChunkSize, $words.Count)
$chunkText = ($words[$i..($end - 1)] -join ' ')
[void]$chunks.Add($chunkText)
$i += $ChunkSize - $Overlap
}
return $chunks
}

function New-RAGIndex {
<# 从指定目录的文档构建 RAG 索引 #>
param(
[string]$Path = '.\docs',
[int]$ChunkSize = 500
)

$store = [LocalVectorStore]::new()
$files = Get-ChildItem -Path $Path -Include '*.md','*.txt' -Recurse

foreach ($file in $files) {
$content = Get-Content $file.FullName -Raw -Encoding UTF8
$chunks = Split-Document -Content $content -ChunkSize $ChunkSize
$chunkIndex = 0
foreach ($chunk in $chunks) {
$embedding = Get-LocalEmbedding -Text $chunk
$store.AddChunk($chunk, "$($file.Name)#chunk-$chunkIndex", $embedding)
$chunkIndex++
}
Write-Host "已索引: $($file.Name) ($($chunks.Count) 个切片)" -ForegroundColor Cyan
}
Write-Host "索引完成,共 $($store.Documents.Count) 个文档切片" -ForegroundColor Green
return $store
}

function Invoke-RAGQuery {
<# 基于 RAG 索引进行检索增强查询 #>
param(
[LocalVectorStore]$Store,
[string]$Question,
[string]$Model = 'qwen2.5:7b',
[int]$TopK = 3
)

$queryEmbedding = Get-LocalEmbedding -Text $Question
$results = $Store.Search($queryEmbedding, $TopK)

$context = ($results | ForEach-Object {
"【来源: $($_.source) | 相关度: $($_.score):N3】`n$($_.content)"
}) -join "`n`n---`n`n"

$messages = @(
@{
role = 'system'
content = '根据以下参考资料回答用户问题。如果资料中没有相关信息,请明确说明。'
}
@{
role = 'user'
content = "参考资料:`n$context`n`n问题:$Question"
}
)

Write-Host "`n检索到 $($results.Count) 条相关文档:" -ForegroundColor Yellow
$results | ForEach-Object {
Write-Host " - $($_.source) (相关度: $($_.score):N3)" -ForegroundColor DarkGray
}

return Invoke-OllamaChat -Model $Model -Messages $messages -Temperature 0.2
}

# 用法示例:构建索引并查询
$store = New-RAGIndex -Path '.\knowledge-base'
$answer = Invoke-RAGQuery -Store $store -Question '公司的密码策略要求是什么?'
Write-Host "`n$answer"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
已索引: security-policy.md (12 个切片)
已索引: operations-guide.md (8 个切片)
已索引: network-diagram.md (5 个切片)
索引完成,共 25 个文档切片

检索到 3 条相关文档:
- security-policy.md#chunk-2 (相关度: 0.892)
- security-policy.md#chunk-7 (相关度: 0.856)
- operations-guide.md#chunk-1 (相关度: 0.743)

根据安全策略文档,公司密码策略要求如下:
1. 密码长度不少于 14 个字符
2. 必须包含大写字母、小写字母、数字和特殊字符中的至少三类
3. 密码有效期 90 天,不可重复最近 12 次使用过的密码
4. 账户锁定策略:连续 5 次输入错误后锁定 30 分钟
5. 管理员账户必须启用多因素认证(MFA)

AI 辅助运维工具

将本地 LLM 与 PowerShell 运维脚本结合,可以构建智能化的日志分析、命令推荐和错误诊断工具。下面这个模块展示了几个实用的运维场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
function Invoke-AILogAnalysis {
<# 使用本地 LLM 分析系统日志 #>
param(
[string]$LogPath = 'C:\Windows\System32\winevt\Logs\System.evtx',
[int]$MaxEvents = 50,
[string]$Model = 'qwen2.5:7b'
)

Write-Host "正在读取最近的 $MaxEvents 条事件日志..." -ForegroundColor Cyan
$events = Get-WinEvent -Path $LogPath -MaxEvents $MaxEvents -ErrorAction SilentlyContinue

if (-not $events) {
$events = Get-WinEvent -LogName System -MaxEvents $MaxEvents
}

$errorEvents = $events | Where-Object { $_.Level -le 3 } |
Select-Object TimeCreated, Id, LevelDisplayName, Message |
Format-Table -AutoSize | Out-String

$prompt = @"
以下是最近的事件日志(仅含错误和警告):

$errorEvents

请分析以下内容:
1. 是否存在异常模式或重复性错误
2. 可能的根因分析
3. 推荐的处理步骤
"@

$messages = @(
@{ role = 'system'; content = '你是一位资深的 Windows 系统管理员,擅长日志分析和故障排查。' }
@{ role = 'user'; content = $prompt }
)

return Invoke-OllamaChat -Model $Model -Messages $messages -Temperature 0.3
}

function Get-AICommandSuggestion {
<# 根据自然语言描述推荐 PowerShell 命令 #>
param(
[Parameter(Mandatory)]
[string]$Description,

[string]$Model = 'qwen2.5:7b'
)

$messages = @(
@{
role = 'system'
content = '你是一个 PowerShell 命令助手。根据用户的自然语言描述,给出 1-3 个 PowerShell 命令方案。每个方案包含命令、参数说明和使用场景。只返回 PowerShell 代码和简短说明。'
}
@{
role = 'user'
content = $Description
}
)

return Invoke-OllamaChat -Model $Model -Messages $messages -Temperature 0.4
}

function Repair-AIScriptError {
<# 使用本地 LLM 诊断和修复脚本错误 #>
param(
[Parameter(Mandatory)]
[string]$ScriptContent,

[Parameter(Mandatory)]
[string]$ErrorMessage,

[string]$Model = 'qwen2.5:7b'
)

$messages = @(
@{
role = 'system'
content = '你是一个 PowerShell 调试专家。分析脚本代码和错误信息,给出问题原因和修复后的完整脚本。'
}
@{
role = 'user'
content = "脚本代码:`n$ScriptContent`n`n错误信息:`n$ErrorMessage"
}
)

return Invoke-OllamaChat -Model $Model -Messages $messages -Temperature 0.2
}

# 用法示例 1:日志分析
$analysis = Invoke-AILogAnalysis -MaxEvents 100
Write-Host $analysis

# 用法示例 2:命令推荐
$suggestion = Get-AICommandSuggestion -Description '查找过去 24 小时内被修改的大于 100MB 的文件'
Write-Host $suggestion

# 用法示例 3:脚本错误修复
$badScript = @'
Get-Process | Where-Object { $_.WorkingSet -gt 100MB } | Select Name, CPU
'@

$errorMsg = 'Cannot compare "System.Byte[]" to "System.Int32"'

$fix = Repair-AIScriptError -ScriptContent $badScript -ErrorMessage $errorMsg
Write-Host $fix

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
正在读取最近的 100 条事件日志...

## 日志分析报告

### 异常模式
1. **重复性磁盘警告**:过去 2 小时内出现 15 次 Disk 警告(Event ID 51),
指向磁盘 \Device\Harddisk1\DR1,表明可能存在磁盘硬件问题。
2. **服务异常终止**:W32Time 服务在 04:30 和 05:15 两次意外终止(Event ID 7034)。

### 根因分析
- 磁盘警告可能与坏道或 SATA 线缆松动有关
- 时间服务崩溃通常与网络连接中断相关

### 推荐处理步骤
1. 立即运行 `chkdsk /r` 检查磁盘健康状态
2. 使用 `smartctl -a /dev/sda` 查看 SMART 信息
3. 检查 W32Time 服务依赖的网络连接
4. 运行 `w32tm /resync` 重新同步时间
1
2
3
4
5
6
7
8
9
## 推荐方案

### 方案一:使用 Get-ChildItem
Get-ChildItem -Path C:\ -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.Length -gt 100MB -and $_.LastWriteTime -gt (Get-Date).AddHours(-24) } |
Select-Object FullName, @{N='SizeMB';E={[math]::Round($_.Length/1MB,2)}}, LastWriteTime |
Sort-Object SizeMB -Descending

适用于全盘扫描,结果详细但速度较慢。

注意事项

  1. 模型选择与硬件匹配:7B 参数模型至少需要 8GB 显存或 16GB 内存,13B 模型建议 16GB 以上显存。使用 ollama list 查看已安装模型,根据硬件配置选择合适的模型大小,避免因内存不足导致推理速度过慢或崩溃。

  2. API 兼容性:Ollama 和 LM Studio 都兼容 OpenAI API 格式,但端口号不同(Ollama 默认 11434,LM Studio 默认 1234)。切换工具时需要修改 URI 前缀,建议将基础 URL 配置为变量或环境变量,便于灵活切换。

  3. RAG 的切片策略:文档切片大小直接影响检索质量。过大的切片包含过多无关信息,过小的切片丢失上下文。对于技术文档建议 300-500 词,对于日志文件建议按时间窗口切分,每片保留完整的上下文信息。

  4. 流式输出与超时处理:长时间推理可能超过 Invoke-RestMethod 的默认超时。流式输出可以避免长时间等待无响应,但需要手动处理 SSE 数据流。建议为非流式调用设置 -TimeoutSec 120 或更长的超时值。

  5. 敏感数据保护:虽然使用本地模型避免了数据外传,但仍需注意日志中可能记录 API 请求内容。在生产环境中建议禁用 PowerShell 的脚本块日志记录(ScriptBlockLogging),并定期清理 Ollama 的会话历史。

  6. 模型回复的不确定性:LLM 生成的代码和建议可能存在错误。对于运维操作(特别是破坏性命令),务必先在测试环境验证,不要直接将 AI 生成的命令粘贴到生产终端执行。建议加入 -WhatIf 参数进行预检。

PowerShell 技能连载 - MCP 协议集成

适用于 PowerShell 7.0 及以上版本

MCP(Model Context Protocol)是 Anthropic 于 2024 年底发布的开放协议,旨在为大语言模型(LLM)提供标准化的上下文获取和工具调用接口。通过 MCP,AI 应用可以统一地连接数据源、调用外部工具、访问资源,而不必为每个集成编写专用的适配代码。协议本身基于 JSON-RPC 2.0,传输层支持 stdio 和 SSE(Server-Sent Events)两种模式,非常适合构建可组合的 AI 工具生态。

对于 PowerShell 用户来说,MCP 带来了一个令人兴奋的可能性:我们可以用 PowerShell 脚本直接构建 MCP 服务端,将系统管理能力以标准化工具的形式暴露给 AI 助手;同时也能编写 MCP 客户端,让 PowerShell 脚本调用任何符合 MCP 规范的 AI 服务和工具集。这种双向集成使得 PowerShell 成为 AI 工具链中一等公民。

本文将通过三个实际示例,展示如何用 PowerShell 7 搭建 MCP 服务端、编写 MCP 客户端,以及打包一套实用的系统管理工具集,帮助读者快速上手 MCP 与 PowerShell 的结合使用。

搭建 MCP 服务端

MCP 服务端的核心职责是:监听客户端请求、注册可用的工具(tools)和资源(resources)、处理工具调用并返回结果。以下代码用 PowerShell 实现了一个基于 stdio 传输的最小化 MCP 服务端,支持协议握手、工具列举和工具调用三个核心能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
using namespace System.Collections.Generic
using namespace System.Text.Json

# MCP 服务端核心类
class McpServer {
[string] $ServerName
[string] $Version
[List[hashtable]] $Tools
[Dictionary[string, scriptblock]] $Handlers

McpServer([string]$Name, [string]$Ver) {
$this.ServerName = $Name
$this.Version = $Ver
$this.Tools = [List[hashtable]]::new()
$this.Handlers = [Dictionary[string, scriptblock]]::new()
}

# 注册工具
[void] RegisterTool(
[string]$ToolName,
[string]$Description,
[hashtable]$Parameters,
[scriptblock]$Handler
) {
$toolDef = @{
name = $ToolName
description = $Description
inputSchema = @{
type = 'object'
properties = $Parameters
required = @($Parameters.Keys)
}
}
$this.Tools.Add($toolDef)
$this.Handlers[$ToolName] = $Handler
}

# 处理 JSON-RPC 请求
[string] HandleRequest([string]$JsonLine) {
$request = $JsonLine | ConvertFrom-Json -AsHashtable
$response = @{ jsonrpc = '2.0'; id = $request.id }

switch ($request.method) {
'initialize' {
$response.result = @{
protocolVersion = '2025-03-26'
capabilities = @{ tools = @{} }
serverInfo = @{
name = $this.ServerName
version = $this.Version
}
}
}
'tools/list' {
$response.result = @{ tools = $this.Tools }
}
'tools/call' {
$toolName = $request.params.name
$args = $request.params.arguments
if ($this.Handlers.ContainsKey($toolName)) {
$result = & $this.Handlers[$toolName] $args
$response.result = @{
content = @(
@{ type = 'text'; text = ($result | ConvertTo-Json -Depth 5) }
)
}
} else {
$response.error = @{
code = -32601
message = "Tool not found: $toolName"
}
}
}
default {
$response.error = @{
code = -32601
message = "Method not found: $($request.method)"
}
}
}
return $response | ConvertTo-Json -Depth 10 -Compress
}

# 启动 stdio 监听循环
[void] Start() {
Write-Host "[$($this.ServerName)] MCP Server started on stdio" `
-ForegroundColor Green
while ($null -ne ($line = [Console]::In.ReadLine())) {
$reply = $this.HandleRequest($line)
[Console]::Out.WriteLine($reply)
[Console]::Out.Flush()
}
}
}

# 创建服务端实例并注册示例工具
$server = [McpServer]::new('ps-mcp-server', '1.0.0')

$server.RegisterTool(
'get-process-info',
'获取指定进程的详细信息',
@{ name = @{ type = 'string'; description = '进程名称' } },
{
param($args)
$procs = Get-Process -Name $args.name -ErrorAction SilentlyContinue
if ($procs) {
$procs | Select-Object Name, Id, CPU, WorkingSet64, Path |
Format-Table -AutoSize | Out-String
} else {
"未找到进程: $($args.name)"
}
}
)

$server.RegisterTool(
'calculate',
'执行数学表达式计算',
@{
expression = @{ type = 'string'; description = '数学表达式' }
},
{
param($args)
$result = Invoke-Expression $args.expression
"计算结果: $($args.expression) = $result"
}
)

# 如需 stdio 模式,取消下行注释
# $server.Start()

上述代码定义了一个 McpServer 类,通过 RegisterTool 方法注册工具及其处理脚本块,HandleRequest 方法根据 JSON-RPC 方法名分发处理。启动后在 stdio 上逐行读取 JSON 请求并返回 JSON 响应。以下模拟了客户端发送 initialize 和 tools/list 请求后服务端返回的响应:

1
2
3
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{}},"serverInfo":{"name":"ps-mcp-server","version":"1.0.0"}}}

{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"get-process-info","description":"获取指定进程的详细信息","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"进程名称"}},"required":["name"]}},{"name":"calculate","description":"执行数学表达式计算","inputSchema":{"type":"object","properties":{"expression":{"type":"string","description":"数学表达式"}},"required":["expression"]}}]}}

编写 MCP 客户端

有了服务端之后,我们需要一个客户端来连接它、发现可用工具并发起调用。以下代码实现了一个 MCP 客户端,通过启动服务端进程并以 stdin/stdout 管道与之通信,完整走通握手、列举、调用的流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
class McpClient {
[System.Diagnostics.Process] $Process
[int] $RequestId = 0
[hashtable] $ServerCapabilities
[array] $AvailableTools

# 启动服务端进程
[void] Connect([string]$Command, [string[]]$Arguments) {
$psi = [System.Diagnostics.ProcessStartInfo]::new()
$psi.FileName = $Command
$psi.Arguments = $Arguments -join ' '
$psi.UseShellExecute = $false
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.CreateNoWindow = $true

$this.Process = [System.Diagnostics.Process]::Start($psi)
Write-Host "已连接到 MCP 服务端 (PID: $($this.Process.Id))" `
-ForegroundColor Cyan

# 发送 initialize 请求
$initResult = $this.SendRequest('initialize', @{
protocolVersion = '2025-03-26'
capabilities = @{}
clientInfo = @{
name = 'ps-mcp-client'
version = '1.0.0'
}
})
$this.ServerCapabilities = $initResult.capabilities
Write-Host "服务端: $($initResult.serverInfo.name) v$($initResult.serverInfo.version)"

# 发送 initialized 通知
$this.SendNotification('notifications/initialized', @{})

# 缓存可用工具列表
$toolsResult = $this.SendRequest('tools/list', @{})
$this.AvailableTools = $toolsResult.tools
Write-Host "已发现 $($this.AvailableTools.Count) 个工具:"
foreach ($tool in $this.AvailableTools) {
Write-Host (" - {0}: {1}" -f $tool.name, $tool.description) `
-ForegroundColor Yellow
}
}

# 发送 JSON-RPC 请求并等待响应
[hashtable] SendRequest([string]$Method, [hashtable]$Params) {
$this.RequestId++
$request = @{
jsonrpc = '2.0'
id = $this.RequestId
method = $Method
params = $Params
}
$jsonLine = $request | ConvertTo-Json -Depth 10 -Compress
$this.Process.StandardInput.WriteLine($jsonLine)
$this.Process.StandardInput.Flush()

$responseLine = $this.Process.StandardOutput.ReadLine()
$response = $responseLine | ConvertFrom-Json -AsHashtable
if ($response.error) {
throw "MCP Error [$($response.error.code)]: $($response.error.message)"
}
return $response.result
}

# 发送 JSON-RPC 通知(无 id,无响应)
[void] SendNotification([string]$Method, [hashtable]$Params) {
$notification = @{
jsonrpc = '2.0'
method = $Method
params = $Params
}
$jsonLine = $notification | ConvertTo-Json -Depth 10 -Compress
$this.Process.StandardInput.WriteLine($jsonLine)
$this.Process.StandardInput.Flush()
}

# 调用指定工具
[string] InvokeTool([string]$ToolName, [hashtable]$Arguments) {
$result = $this.SendRequest('tools/call', @{
name = $ToolName
arguments = $Arguments
})
$textParts = $result.content | Where-Object { $_.type -eq 'text' }
return ($textParts | ForEach-Object { $_.text }) -join "`n"
}

# 断开连接
[void] Disconnect() {
if ($this.Process -and -not $this.Process.HasExited) {
$this.Process.StandardInput.Close()
$this.Process.WaitForExit(5000)
if (-not $this.Process.HasExited) {
$this.Process.Kill()
}
}
Write-Host "已断开 MCP 连接" -ForegroundColor DarkGray
}
}

客户端通过 Connect 方法启动服务端进程并完成协议握手,InvokeTool 方法封装了工具调用的细节,调用者只需传入工具名和参数即可获取结果。下面演示了使用客户端调用工具的典型流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
已连接到 MCP 服务端 (PID: 45832)
服务端: ps-mcp-server v1.0.0
已发现 2 个工具:
- get-process-info: 获取指定进程的详细信息
- calculate: 执行数学表达式计算

PS> $client.InvokeTool('get-process-info', @{ name = 'pwsh' })

Name Id CPU WorkingSet64 Path
---- -- --- ------------ ----
pwsh 45832 3.2145 83251200 /usr/local/bin/pwsh
pwsh 45901 1.7823 79216640 /usr/local/bin/pwsh

PS> $client.InvokeTool('calculate', @{ expression = '2 ** 10 + sqrt(144)' })
计算结果: 2 ** 10 + sqrt(144) = 1012

实用系统管理工具集

理解了 MCP 的客户端-服务端架构后,我们可以把日常系统管理中常用的操作封装成 MCP 工具集,让 AI 助手能够直接调用这些能力。以下代码注册了一组涵盖系统信息、文件操作和网络诊断的实用工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
$sysServer = [McpServer]::new('ps-sys-tools', '1.0.0')

# 工具 1:系统概览
$sysServer.RegisterTool(
'system-overview',
'获取操作系统、CPU、内存等系统概览信息',
@{},
{
param($args)
$os = [System.Environment]::OSVersion
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
$mem = Get-CimInstance Win32_OperatingSystem
$totalMem = [math]::Round($mem.TotalVisibleMemorySize / 1MB, 2)
$freeMem = [math]::Round($mem.FreePhysicalMemory / 1MB, 2)
$usedPct = [math]::Round(($totalMem - $freeMem) / $totalMem * 100, 1)
@{
OS = "$($os.Platform) $($os.Version)"
MachineName = [System.Environment]::MachineName
Processor = $cpu.Name
CPUUtilization = "$([math]::Round($cpu.LoadPercentage, 1))%"
TotalMemory_GB = $totalMem
FreeMemory_GB = $freeMem
MemoryUsage = "$usedPct%"
PowerShellVersion= $PSVersionTable.PSVersion.ToString()
Uptime = (Get-Date) - $mem.LastBootUpTime | Out-String
}
}
)

# 工具 2:搜索文件内容
$sysServer.RegisterTool(
'search-files',
'在指定目录中搜索包含指定文本的文件',
@{
directory = @{ type = 'string'; description = '搜索目录路径' }
pattern = @{ type = 'string'; description = '搜索的文本模式' }
extension = @{
type = 'string'
description = '文件扩展名过滤(可选)'
}
},
{
param($args)
$params = @{
Path = $args.directory
Pattern = $args.pattern
Recurse = $true
}
if ($args.extension) {
$params.Include = "*.$($args.extension)"
}
$results = Select-String @params |
Select-Object -First 20 |
ForEach-Object {
@{
File = $_.Path
Line = $_.LineNumber
Match = $_.Line.Trim()
}
}
if (-not $results) {
return @{ message = '未找到匹配结果'; matches = @() }
}
@{ matches = $results; totalFound = $results.Count }
}
)

# 工具 3:网络连通性诊断
$sysServer.RegisterTool(
'network-diag',
'对指定主机执行网络连通性诊断(Ping + 端口检测)',
@{
hostname = @{ type = 'string'; description = '目标主机名或 IP' }
ports = @{
type = 'string'
description = '要检测的端口,逗号分隔(如 80,443,22)'
}
},
{
param($args)
$results = @{}

# Ping 测试
$ping = Test-Connection -TargetName $args.hostname `
-Count 4 -ErrorAction SilentlyContinue
if ($ping) {
$latencies = $ping | ForEach-Object { $_.Latency }
$results.ping = @{
success = $true
avgMs = [math]::Round(($latencies | Measure-Object -Average).Average, 2)
minMs = ($latencies | Measure-Object -Minimum).Minimum
maxMs = ($latencies | Measure-Object -Maximum).Maximum
}
} else {
$results.ping = @{ success = $false; message = '主机不可达' }
}

# 端口检测
$portResults = @{}
foreach ($port in $args.port -split ',') {
$port = $port.Trim()
$tcp = [System.Net.Sockets.TcpClient]::new()
try {
$connect = $tcp.ConnectAsync($args.hostname, [int]$port)
$wait = $connect.Wait(3000)
$portResults[$port] = if ($wait -and $connect.IsCompletedSuccessfully) {
'OPEN'
} else {
'CLOSED/TIMEOUT'
}
} finally {
$tcp.Close()
}
}
$results.ports = $portResults
$results
}
)

# 工具 4:服务状态查询
$sysServer.RegisterTool(
'get-service-status',
'查询指定服务的运行状态',
@{
name = @{ type = 'string'; description = '服务名称(支持通配符)' }
},
{
param($args)
$services = Get-Service -Name $args.name -ErrorAction SilentlyContinue
if (-not $services) {
return @{ message = "未找到服务: $($args.name)" }
}
$services | ForEach-Object {
@{
Name = $_.Name
DisplayName= $_.DisplayName
Status = $_.Status.ToString()
StartType = $_.StartType.ToString()
}
}
}
)

Write-Host "已注册 $($sysServer.Tools.Count) 个系统管理工具" `
-ForegroundColor Green
Write-Host "工具列表:"
$sysServer.Tools | ForEach-Object {
Write-Host (" {0,-20} {1}" -f $_.name, $_.description)
}

注册完成后,AI 助手可以通过 MCP 协议调用这些工具。例如,当用户询问”这台服务器的内存够用吗”时,AI 会自动调用 system-overview 工具获取实时数据并给出分析。以下是一次典型调用的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
PS> $result = $sysServer.HandleRequest(
'{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"network-diag","arguments":{"hostname":"example.com","ports":"80,443,22"}}}'
)

$result | ConvertFrom-Json | ConvertTo-Json -Depth 5

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\"ping\":{\"success\":true,\"avgMs\":23.45,\"minMs\":21,\"maxMs\":28},\"ports\":{\"80\":\"OPEN\",\"443\":\"OPEN\",\"22\":\"CLOSED/TIMEOUT\"}}"
}
]
}
}

PS> $result2 = $sysServer.HandleRequest(
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"system-overview","arguments":{}}}'
)

$result2 | ConvertFrom-Json -AsHashtable | ForEach-Object {
$_.result.content[0].text | ConvertFrom-Json
}

OS : Unix 15.4.0
MachineName : workstation01
Processor : Apple M3 Pro
CPUUtilization : 12.3%
TotalMemory_GB : 36.0
FreeMemory_GB : 14.27
MemoryUsage : 60.4%
PowerShellVersion: 7.5.0
Uptime : 3.14:27:08.3421000

注意事项

  1. 传输模式选择:stdio 模式适合进程间通信(AI 助手启动 PowerShell 子进程),SSE 模式适合跨网络通信(远程服务器上的 MCP 服务)。Windows 环境下推荐 stdio,Linux 服务端部署可考虑 SSE。

  2. 安全沙箱:MCP 工具本质上是在执行任意 PowerShell 代码,必须在受控环境中运行。生产部署时应限制工具的执行权限,避免注册如 Invoke-Expression 等高风险操作,或使用 constrained language mode 加固。

  3. 超时处理:网络诊断等工具可能耗时较长,建议在客户端实现超时机制(如 WaitHandle.AsyncWaitHandle.WaitOne(5000)),避免长时间阻塞导致 JSON-RPC 管道挂死。

  4. 错误传播:MCP 规范定义了标准错误码(如 -32600 Invalid Request、-32601 Method not found),服务端应遵循这些错误码返回,客户端应正确捕获并呈现错误信息,不要将原始异常暴露给 AI。

  5. 工具描述的重要性description 字段是 AI 模型选择工具的主要依据。描述应当清晰、具体、包含关键的使用前提。例如”获取指定进程的详细信息”比”查询进程”更有利于模型准确匹配用户意图。

  6. 序列化深度:PowerShell 对象通常嵌套层级较深(如 CIM 实例、进程对象),使用 ConvertTo-Json 时务必指定足够的 -Depth 参数(建议 5 以上),否则深层属性会被截断为字符串”$ref”,导致 AI 收到不完整的数据。

PowerShell 技能连载 - AI Agent 自动化框架

适用于 PowerShell 7.0 及以上版本

AI Agent(智能代理)是当前大语言模型应用的热门方向。与传统的”单次问答”不同,Agent 能够自主规划任务步骤、调用外部工具、根据执行结果进行推理,最终完成复杂目标。对于系统运维工程师来说,这意味着可以将 LLM 的理解能力与 PowerShell 强大的系统管理能力结合起来,构建出真正”懂意图”的自动化框架。

PowerShell 作为 Windows/Linux/macOS 通用的脚本语言,天生具备丰富的系统管理 cmdlet(如文件操作、进程管理、网络请求、注册表读写等),这些都可以作为 Agent 的”工具”暴露给 LLM。通过精心设计的工具调用协议,Agent 可以根据用户的自然语言描述,自动选择合适的命令并执行。

本文将分三个部分逐步构建一个轻量级 AI Agent 框架:首先实现与 LLM API 的对话集成,然后定义工具调用机制,最后实现 ReAct(Reasoning + Acting)循环,使 Agent 具备多步推理和自主执行的能力。

LLM API 集成

Agent 的核心是语言模型。我们首先封装一个通用的 LLM 调用函数,支持 OpenAI 兼容的 API(包括 OpenAI 官方、Azure OpenAI、以及 Ollama 等本地部署的模型)。该函数负责构建对话上下文、发送请求并解析响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
function Invoke-LLMChat {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[array]$Messages,

[Parameter()]
[string]$Model = 'gpt-4o-mini',

[Parameter()]
[string]$BaseUrl = 'https://api.openai.com/v1',

[Parameter()]
[string]$ApiKey = $env:OPENAI_API_KEY,

[Parameter()]
[double]$Temperature = 0.3,

[Parameter()]
[array]$Tools
)

$headers = @{
'Content-Type' = 'application/json'
'Authorization' = "Bearer $ApiKey"
}

$body = @{
model = $Model
messages = $Messages
temperature = $Temperature
}

if ($Tools) {
$body['tools'] = $Tools
$body['tool_choice'] = 'auto'
}

$uri = "$BaseUrl/chat/completions"
$response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body ($body | ConvertTo-Json -Depth 10)

return $response.choices[0].message
}

# 构建系统提示词,定义 Agent 的角色和行为规范
$systemPrompt = @"
你是一个 PowerShell 运维 Agent。你可以使用提供的工具来执行系统管理任务。
请根据用户的请求,选择合适的工具进行操作。每次只调用一个工具。
如果任务需要多个步骤,请逐步完成。操作完成后请给出简洁的总结。
"@

# 初始化对话历史
$script:conversationHistory = @(
@{ role = 'system'; content = $systemPrompt }
)

上面的代码定义了 Invoke-LLMChat 函数,它接受对话消息数组、模型名称和可选的工具定义。通过 $env:OPENAI_API_KEY 环境变量读取 API 密钥,方便切换不同的 API 提供商。

执行结果示例:

1
2
3
4
PS> Invoke-LLMChat -Messages @(@{role='user';content='你好'}) -Model 'gpt-4o-mini'

role : assistant
content : 你好!我是 PowerShell 运维 Agent,可以帮助你管理系统。请问有什么需要?

工具调用框架

接下来定义 Agent 可用的工具集。每个工具包含名称、描述和参数定义(遵循 JSON Schema 格式),以及对应的 PowerShell 执行函数。当 LLM 决定调用某个工具时,我们会解析其返回的函数调用请求,执行对应的 PowerShell 命令,并将结果反馈给模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# 定义可用的工具列表(OpenAI function calling 格式)
$script:agentTools = @(
@{
type = 'function'
function = @{
name = 'get_system_info'
description = '获取当前系统的基本信息,包括操作系统版本、CPU、内存、磁盘使用情况'
parameters = @{
type = 'object'
properties = @{
details = @{
type = 'boolean'
description = '是否返回详细信息'
}
}
}
}
}
@{
type = 'function'
function = @{
name = 'list_processes'
description = '列出当前运行的进程,可按名称筛选并排序'
parameters = @{
type = 'object'
properties = @{
name = @{
type = 'string'
description = '按进程名称筛选(支持通配符)'
}
top = @{
type = 'integer'
description = '返回前 N 个结果,默认 10'
}
sortBy = @{
type = 'string'
enum = @('CPU', 'Memory', 'Name')
description = '排序依据'
}
}
}
}
}
@{
type = 'function'
function = @{
name = 'read_file_content'
description = '读取指定路径的文件内容'
parameters = @{
type = 'object'
properties = @{
path = @{
type = 'string'
description = '文件路径'
}
lastN = @{
type = 'integer'
description = '只读取最后 N 行'
}
}
required = @('path')
}
}
}
)

# 工具执行分发器:根据工具名调用对应的 PowerShell 实现
function Invoke-AgentTool {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ToolName,

[Parameter(Mandatory)]
[hashtable]$Arguments
)

switch ($ToolName) {
'get_system_info' {
$os = Get-CimInstance Win32_OperatingSystem
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
$disks = Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3'

$result = @{
OS = $os.Caption
Version = $os.Version
CPU = $cpu.Name
TotalMemGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 2)
FreeMemGB = [math]::Round($os.FreePhysicalMemory / 1MB, 2)
Disks = $disks | ForEach-Object {
@{
Drive = $_.DeviceID
FreeGB = [math]::Round($_.FreeSpace / 1GB, 2)
TotalGB = [math]::Round($_.Size / 1GB, 2)
UsedPct = [math]::Round(($_.Size - $_.FreeSpace) / $_.Size * 100, 1)
}
}
}
return ($result | ConvertTo-Json -Depth 5)
}

'list_processes' {
$procs = Get-Process
if ($Arguments.name) {
$procs = $procs | Where-Object { $_.Name -like $Arguments.name }
}
$sortField = if ($Arguments.sortBy -eq 'Memory') { 'WorkingSet64' }
elseif ($Arguments.sortBy -eq 'CPU') { 'CPU' }
else { 'Name' }
$top = if ($Arguments.top) { $Arguments.top } else { 10 }

$result = $procs |
Sort-Object -Property $sortField -Descending |
Select-Object -First $top |
ForEach-Object {
@{
Name = $_.Name
PID = $_.Id
CPU_s = [math]::Round($_.CPU, 2)
MemoryMB = [math]::Round($_.WorkingSet64 / 1MB, 2)
}
}
return ($result | ConvertTo-Json -Depth 3)
}

'read_file_content' {
$path = $Arguments.path
if (-not (Test-Path $path)) {
return "错误:文件 '$path' 不存在"
}
$content = Get-Content $path -Encoding UTF8
if ($Arguments.lastN -gt 0) {
$content = $content | Select-Object -Last $Arguments.lastN
}
return ($content -join "`n")
}

default {
return "错误:未知工具 '$ToolName'"
}
}
}

这段代码定义了三个实用工具:get_system_info 获取系统状态、list_processes 管理进程、read_file_content 读取文件。Invoke-AgentTool 函数作为分发器,根据工具名路由到对应的实现逻辑。

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
PS> Invoke-AgentTool -ToolName 'get_system_info' -Arguments @{}

{
"OS": "Microsoft Windows 11 Pro",
"Version": "10.0.26100",
"CPU": "AMD Ryzen 9 7950X",
"TotalMemGB": 31.73,
"FreeMemGB": 14.25,
"Disks": [
{ "Drive": "C:", "FreeGB": 234.5, "TotalGB": 512.0, "UsedPct": 54.2 },
{ "Drive": "D:", "FreeGB": 876.1, "TotalGB": 1024.0, "UsedPct": 14.4 }
]
}

ReAct 循环实现

现在将 LLM 和工具调用结合起来,实现 ReAct(Reasoning + Acting)循环。Agent 在每一步都会思考当前状态、选择一个工具执行、观察执行结果,然后决定下一步行动,直到任务完成或达到最大步数限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
function Start-AgentReAct {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$UserQuery,

[Parameter()]
[int]$MaxSteps = 10,

[Parameter()]
[string]$Model = 'gpt-4o-mini',

[Parameter()]
[string]$BaseUrl = 'https://api.openai.com/v1',

[Parameter()]
[switch]$Verbose
)

# 初始化对话上下文
$history = [System.Collections.ArrayList]::new()
[void]$history.Add(@{ role = 'system'; content = $systemPrompt })
[void]$history.Add(@{ role = 'user'; content = $UserQuery })

Write-Host "`n=== Agent 启动 ===" -ForegroundColor Cyan
Write-Host "任务: $UserQuery`n" -ForegroundColor Yellow

for ($step = 1; $step -le $MaxSteps; $step++) {
Write-Host "--- 步骤 $step/$MaxSteps ---" -ForegroundColor DarkGray

# 调用 LLM 进行推理
$response = Invoke-LLMChat -Messages $history -Model $Model `
-BaseUrl $BaseUrl -Tools $script:agentTools

# 如果模型直接返回文本回复(没有工具调用),说明任务已完成
if (-not $response.tool_calls) {
Write-Host "`n=== Agent 完成 ===" -ForegroundColor Green
Write-Host "最终回复: $($response.content)" -ForegroundColor White
return $response.content
}

# 将助手消息(含工具调用请求)加入历史
[void]$history.Add($response)

# 处理每个工具调用
foreach ($toolCall in $response.tool_calls) {
$toolName = $toolCall.function.name
$toolArgs = $toolCall.function.arguments | ConvertFrom-Json -AsHashtable

if ($Verbose) {
Write-Host "调用工具: $toolName" -ForegroundColor Magenta
Write-Host "参数: $($toolArgs | ConvertTo-Json -Compress)" -ForegroundColor DarkGray
}

# 执行工具
$toolOutput = Invoke-AgentTool -ToolName $toolName -Arguments $toolArgs

if ($Verbose) {
$preview = if ($toolOutput.Length -gt 200) {
$toolOutput.Substring(0, 200) + '...'
} else {
$toolOutput
}
Write-Host "结果: $preview" -ForegroundColor DarkGray
}

# 将工具执行结果反馈给模型
[void]$history.Add(@{
role = 'tool'
tool_call_id = $toolCall.id
content = $toolOutput
})
}
}

Write-Host "`n=== 达到最大步数限制 ===" -ForegroundColor Red
Write-Host "Agent 未能在 $MaxSteps 步内完成任务。" -ForegroundColor Red
}

这段代码实现了完整的 ReAct 循环。每次迭代中,Agent 先向 LLM 发送当前对话历史和可用工具列表,LLM 决定是直接回复还是调用工具。如果调用了工具,执行后将结果追加到对话历史中,继续下一轮推理。

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
PS> Start-AgentReAct -UserQuery '检查系统磁盘空间是否充足,如果 C 盘使用率超过 80%,列出占用内存最多的 5 个进程' -Verbose

=== Agent 启动 ===
任务: 检查系统磁盘空间是否充足,如果 C 盘使用率超过 80%,列出占用内存最多的 5 个进程

--- 步骤 1/10 ---
调用工具: get_system_info
参数: {"details":true}
结果: {"OS":"Microsoft Windows 11 Pro","Version":"10.0.26100","CPU":"AMD Ryzen 9 7950X",...

--- 步骤 2/10 ---
调用工具: list_processes
参数: {"sortBy":"Memory","top":5}
结果: [{"Name":"chrome","PID":12804,"CPU_s":342.56,"MemoryMB":812.34},...

--- 步骤 3/10 ---

=== Agent 完成 ===
最终回复: 系统磁盘状态良好。C 盘使用率 54.2%,未超过 80% 阈值。
不过我仍然为你列出了占用内存最多的 5 个进程:
1. chrome (PID 12804) - 812.3 MB
2. Code (PID 9216) - 654.1 MB
3. msedge (PID 4452) - 423.7 MB
4. PowerShell (PID 7780) - 287.4 MB
5. docker (PID 3308) - 198.2 MB

注意事项

  1. API 密钥安全:切勿将 API 密钥硬编码在脚本中,应通过环境变量(如 $env:OPENAI_API_KEY)或 Azure Key Vault 等密钥管理服务获取,避免密钥泄露。

  2. 工具执行权限:Agent 调用的工具具有与运行脚本相同的权限。在生产环境中,务必对工具实现添加权限校验和白名单机制,防止 Agent 执行危险操作(如删除关键文件、修改系统配置)。

  3. 循环步数限制:ReAct 循环必须设置 MaxSteps 上限,防止 LLM 陷入无限循环。建议根据任务复杂度设置为 5-15 步,并在达到上限时给出明确的告警信息。

  4. 本地模型支持:如果使用 Ollama 等本地模型,只需将 BaseUrl 改为 http://localhost:11434/v1ApiKey 设为 ollama 即可。但本地模型的工具调用能力可能不如 GPT-4 系列稳定,建议充分测试。

  5. 错误处理与重试:网络请求可能因超时或限流失败。建议在 Invoke-LLMChat 中添加指数退避重试逻辑,并对工具执行结果进行异常捕获,将错误信息反馈给 Agent 以便自我修正。

  6. 对话历史管理:长对话会消耗大量 Token。实际使用时应实现滑动窗口或摘要机制,在保留关键上下文的同时控制历史消息长度,降低 API 调用成本。

PowerShell 技能连载 - 本地大模型运维助手

适用于 PowerShell 7.0 及以上版本,需要安装 Ollama

数据安全和隐私合规要求使得很多企业无法使用云端 AI 服务。本地部署的大语言模型(如通过 Ollama 运行的 Llama、Qwen、DeepSeek 等)提供了完全内网的 AI 能力——日志智能分析、故障根因推理、脚本自动生成、配置合规审查,所有数据不出内网。结合 PowerShell 的系统管理能力和本地 AI 的推理能力,可以构建强大的智能运维助手。

本文将讲解如何使用 PowerShell 构建基于本地大模型的运维自动化工具。

Ollama 环境准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 检查 Ollama 状态
function Test-OllamaStatus {
try {
$response = Invoke-RestMethod -Uri "http://localhost:11434/api/tags" -TimeoutSec 5
Write-Host "Ollama 运行中" -ForegroundColor Green
Write-Host "可用模型:" -ForegroundColor Cyan
foreach ($model in $response.models) {
$sizeGB = [math]::Round($model.size / 1GB, 2)
Write-Host " $($model.name) ($sizeGB GB) - 修改于 $($model.modified_at.Substring(0, 10))"
}
return $true
} catch {
Write-Host "Ollama 未运行" -ForegroundColor Red
Write-Host "启动方式:ollama serve" -ForegroundColor Yellow
return $false
}
}

Test-OllamaStatus

# 拉取模型
function Install-OllamaModel {
param([Parameter(Mandatory)][string]$ModelName)

Write-Host "拉取模型:$ModelName(可能需要较长时间)..." -ForegroundColor Cyan
$body = @{ name = $ModelName } | ConvertTo-Json

Invoke-RestMethod -Uri "http://localhost:11434/api/pull" `
-Method Post -Body $body -ContentType "application/json"
Write-Host "模型已下载:$ModelName" -ForegroundColor Green
}

# Install-OllamaModel -ModelName "qwen2.5-coder:7b"

执行结果示例:

1
2
3
4
Ollama 运行中
可用模型:
llama3.2:latest (2.02 GB) - 修改于 2025-07-15
qwen2.5-coder:7b (4.67 GB) - 修改于 2025-07-20

本地 AI 调用封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function Invoke-LocalAI {
<#
.SYNOPSIS
调用本地 Ollama 模型
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Prompt,

[string]$SystemPrompt = "你是一个 PowerShell 运维专家。回答简洁,用中文。",

[string]$Model = "qwen2.5-coder:7b",

[double]$Temperature = 0.3,

[switch]$ShowProgress
)

$body = @{
model = $Model
system = $SystemPrompt
prompt = $Prompt
stream = $false
options = @{
temperature = $Temperature
num_predict = 2048
}
} | ConvertTo-Json -Depth 5

if ($ShowProgress) {
Write-Host "思考中..." -ForegroundColor DarkGray -NoNewline
}

try {
$response = Invoke-RestMethod -Uri "http://localhost:11434/api/generate" `
-Method Post `
-Body $body `
-ContentType "application/json; charset=utf-8" `
-TimeoutSec 120

if ($ShowProgress) { Write-Host "`r完成! " -ForegroundColor Green }

return $response.response
} catch {
if ($ShowProgress) { Write-Host "`r失败! " -ForegroundColor Red }
throw "AI 调用失败:$($_.Exception.Message)"
}
}

# 基础使用
$answer = Invoke-LocalAI -Prompt "如何在 PowerShell 中获取所有已停止的自动启动服务?" -ShowProgress
Write-Host $answer

执行结果示例:

1
2
3
4
完成!
可以使用以下命令获取所有已停止的自动启动服务:

Get-Service | Where-Object { $_.StartType -eq 'Automatic' -and $_.Status -ne 'Running' }

智能日志分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function Invoke-AILogAnalysis {
<#
.SYNOPSIS
使用本地 AI 分析日志文件
#>
param(
[Parameter(Mandatory)]
[string]$LogPath,

[int]$TailLines = 50,

[string]$Model = "qwen2.5-coder:7b"
)

Write-Host "分析日志:$LogPath" -ForegroundColor Cyan

$content = Get-Content $LogPath -Tail $TailLines -ErrorAction Stop
$logText = $content -join "`n"

$prompt = @"
请分析以下服务器日志,找出:
1. 关键错误和异常
2. 可能的根因
3. 建议的修复步骤

日志内容(最近 $TailLines 行):
$logText
"@

$analysis = Invoke-LocalAI -Prompt $prompt -Model $Model `
-SystemPrompt "你是一个资深运维工程师,擅长日志分析和故障排查。用中文回答,结构清晰。" `
-Temperature 0.2

Write-Host "`n=== AI 日志分析报告 ===" -ForegroundColor Cyan
Write-Host $analysis
return $analysis
}

# 分析应用日志
Invoke-AILogAnalysis -LogPath "C:\Logs\MyApp\app.log" -TailLines 30

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
分析日志:C:\Logs\MyApp\app.log

=== AI 日志分析报告 ===
1. 关键错误:
- 14:30:15 数据库连接超时(Timeout expired)
- 14:35:20 消息队列写入失败
- 14:40:30 服务健康检查失败

2. 可能根因:
- 数据库连接池耗尽,导致后续操作级联失败
- 连接池 Max Pool Size=100,但并发请求峰值达到 200+

3. 修复建议:
- 增大连接池:Max Pool Size=200
- 添加连接超时重试逻辑
- 检查是否有未正确关闭的数据库连接

配置合规审查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function Invoke-AIConfigReview {
<#
.SYNOPSIS
使用 AI 审查配置文件的安全性
#>
param(
[Parameter(Mandatory)]
[string]$ConfigPath
)

$content = Get-Content $ConfigPath -Raw -ErrorAction Stop
$fileName = Split-Path $ConfigPath -Leaf

$prompt = @"
请审查以下配置文件($fileName),检查:
1. 安全风险(硬编码密码、不安全的协议、过弱的加密)
2. 性能问题(不合理的超时、缓存配置)
3. 最佳实践建议

配置文件内容:
$content
"@

$review = Invoke-LocalAI -Prompt $prompt `
-SystemPrompt "你是安全审计专家,专注于配置文件安全审查。用中文回答,按风险等级排序。" `
-Temperature 0.1

Write-Host "`n=== 配置审查报告:$fileName ===" -ForegroundColor Cyan
Write-Host $review
}

# 审查配置文件
Invoke-AIConfigReview -ConfigPath "C:\MyApp\appsettings.json"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
=== 配置审查报告:appsettings.json ===
安全风险(高):
- 发现硬编码的数据库密码:ConnectionString 中包含 P@ssw0rd
建议:使用环境变量或 Azure Key Vault

安全风险(中):
- CORS 配置允许所有来源:AllowAnyOrigin = true
建议:限制为具体域名

最佳实践建议:
- 建议启用 HTTPS 严格传输安全(HSTS)
- 日志级别在生产环境应为 Warning,当前为 Debug

批量运维辅助

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# AI 辅助生成运维脚本
function Get-AIScript {
param(
[Parameter(Mandatory)]
[string]$Description
)

$prompt = @"
请根据以下描述生成一个 PowerShell 脚本。要求:
1. 包含参数验证
2. 包含错误处理(try/catch)
3. 使用 Write-Host 输出带颜色的状态信息
4. 代码简洁实用,不要过度设计

需求描述:$Description
"@

$script = Invoke-LocalAI -Prompt $prompt `
-SystemPrompt "你是 PowerShell 脚本专家。只输出代码,不要解释。" `
-Temperature 0.2

return $script
}

# 生成脚本
$generated = Get-AIScript -Description "监控多个远程服务器的 CPU 和内存使用率,超过阈值时输出告警"
Write-Host $generated

# AI 辅助解释命令
function Get-AIExplanation {
param([Parameter(Mandatory)][string]$Command)

$explanation = Invoke-LocalAI -Prompt "简要解释以下 PowerShell 命令的作用:$Command" `
-Temperature 0.1

Write-Host $explanation
}

Get-AIExplanation -Command "Get-ChildItem | Where-Object { $_.Length -gt 1MB } | Sort-Object Length -Descending | Select-Object -First 10 Name, @{N='SizeMB';E={[math]::Round($_.Length/1MB,1)}}"

执行结果示例:

1
这个命令在当前目录中查找大于 1MB 的文件,按大小降序排列,显示最大的 10 个文件的名称和大小(MB)。

注意事项

  1. 资源需求:本地 LLM 需要大量内存/显存,7B 模型至少需要 8GB 内存,13B 需要 16GB+
  2. 推理速度:没有 GPU 时推理较慢(CPU 模式每秒几 token),生产环境建议使用 GPU
  3. 模型选择:运维场景推荐代码类模型(Qwen-Coder、DeepSeek-Coder),通用能力更强
  4. 提示词工程:AI 输出质量取决于提示词质量,提供明确的上下文和格式要求
  5. 输出验证:AI 生成的脚本必须经过人工审查和测试,不能盲目执行
  6. 数据隐私:虽然数据不出内网,但仍需注意不要将敏感信息(密码、密钥)传入 AI

PowerShell 技能连载 - AI 服务集成

适用于 PowerShell 5.1 及以上版本,调用 AI API 需要网络访问和 API 密钥

2025 年,大语言模型(LLM)已经从实验性工具变成了生产力基础设施的一部分。PowerShell 作为自动化运维的核心语言,可以很自然地与 AI 服务集成——用自然语言生成运维脚本、让 AI 分析日志错误、构建智能告警系统、甚至搭建本地的 AI 助手。无论是调用云端 API(OpenAI、Azure OpenAI)还是本地模型(Ollama),PowerShell 都能胜任。

本文将讲解如何在 PowerShell 中调用 AI 服务、处理响应流,以及构建实用的 AI 驱动运维工具。

调用 OpenAI API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function Invoke-OpenAIChat {
<#
.SYNOPSIS
调用 OpenAI Chat Completion API
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Prompt,

[string]$SystemPrompt = "你是一个 PowerShell 运维专家,回答简洁准确。",

[string]$Model = "gpt-4o-mini",

[int]$MaxTokens = 2048,

[double]$Temperature = 0.3
)

$apiKey = $env:OPENAI_API_KEY
if (-not $apiKey) {
throw "请设置环境变量 OPENAI_API_KEY"
}

$body = @{
model = $Model
messages = @(
@{ role = "system"; content = $SystemPrompt },
@{ role = "user"; content = $Prompt }
)
max_tokens = $MaxTokens
temperature = $Temperature
} | ConvertTo-Json -Depth 5

$response = Invoke-RestMethod -Uri "https://api.openai.com/v1/chat/completions" `
-Method Post `
-Headers @{ "Authorization" = "Bearer $apiKey" } `
-ContentType "application/json; charset=utf-8" `
-Body ([System.Text.Encoding]::UTF8.GetBytes($body))

return $response.choices[0].message.content
}

# 示例:让 AI 分析错误日志
$errorLog = Get-Content "C:\Logs\app-error.log" -Tail 20 -ErrorAction SilentlyContinue
if ($errorLog) {
$analysis = Invoke-OpenAIChat -Prompt "分析以下错误日志,找出根本原因并给出修复建议:`n$($errorLog -join "`n")"
Write-Host $analysis -ForegroundColor Cyan
}

# 示例:生成 PowerShell 脚本
$script = Invoke-OpenAIChat -Prompt "写一个 PowerShell 脚本:监控指定目录的磁盘空间,低于阈值时发送邮件告警" -Temperature 0.2
Write-Host $script

执行结果示例:

1
2
3
4
5
6
7
8
根据日志分析,根本原因是数据库连接池耗尽:
1. 错误 "Timeout expired" 在 14:30-14:45 期间集中出现
2. 连接池大小设置为 100,但并发请求峰值达到 200+

修复建议:
1. 增大连接池大小到 Max Pool Size=200
2. 添加连接超时重试逻辑
3. 检查是否有未正确关闭的数据库连接

调用本地 Ollama 模型

对于内网环境或数据敏感场景,可以使用 Ollama 在本地运行模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
function Invoke-OllamaChat {
<#
.SYNOPSIS
调用本地 Ollama API
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Prompt,

[string]$Model = "llama3.2",

[string]$BaseUrl = "http://localhost:11434",

[switch]$Stream
)

$body = @{
model = $Model
prompt = $Prompt
stream = $Stream.IsPresent
} | ConvertTo-Json

if ($Stream) {
# 流式输出
$response = Invoke-WebRequest -Uri "$BaseUrl/api/generate" `
-Method Post `
-ContentType "application/json" `
-Body $body `
-UseBasicParsing

$lines = $response.Content -split "`n" | Where-Object { $_.Trim() }
foreach ($line in $lines) {
$json = $line | ConvertFrom-Json
Write-Host $json.response -NoNewline
}
Write-Host
} else {
$response = Invoke-RestMethod -Uri "$BaseUrl/api/generate" `
-Method Post `
-ContentType "application/json" `
-Body $body

return $response.response
}
}

# 先检查 Ollama 是否运行
try {
$models = Invoke-RestMethod -Uri "http://localhost:11434/api/tags"
Write-Host "Ollama 可用模型:" -ForegroundColor Green
$models.models | ForEach-Object { Write-Host " - $($_.name) ($($_.size / 1GB -as [int]) GB)" }
} catch {
Write-Host "Ollama 未运行,请先启动:ollama serve" -ForegroundColor Yellow
}

# 本地 AI 分析
$result = Invoke-OllamaChat -Prompt "解释以下 PowerShell 命令的作用:Get-ChildItem | Where-Object { $_.LastWriteTime -gt (Get-Date).AddDays(-7) } | Sort-Object Length -Descending | Select-Object -First 10"
Write-Host $result

执行结果示例:

1
2
3
4
Ollama 可用模型:
- llama3.2:latest (2 GB)
- qwen2.5-coder:7b (4 GB)
这个命令查找当前目录下最近 7 天内修改过的文件,按文件大小降序排列,显示最大的 10 个文件。

AI 驱动的智能告警

结合 AI 和监控数据,构建智能告警系统:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
function Send-SmartAlert {
<#
.SYNOPSIS
AI 增强的智能告警
#>
[CmdletBinding()]
param(
[string]$MetricName,
[double]$CurrentValue,
[double]$Threshold,
[string]$Context
)

$severity = if ($CurrentValue -gt $Threshold * 2) { "严重" } elseif ($CurrentValue -gt $Threshold) { "警告" } else { "正常" }

if ($severity -eq "正常") { return }

$prompt = @"
运维告警信息:
- 指标:$MetricName
- 当前值:$CurrentValue
- 阈值:$Threshold
- 严重程度:$severity
- 上下文:$Context

请简要分析可能的原因,并给出 2-3 条排查建议。回复用中文,不超过 200 字。
"@

try {
$aiAdvice = Invoke-OllamaChat -Model "llama3.2" -Prompt $prompt
} catch {
$aiAdvice = "AI 分析不可用,请人工排查。"
}

$alertMsg = @"
[$severity] $MetricName 告警
当前值:$CurrentValue(阈值:$Threshold

AI 分析:
$aiAdvice

时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
"@

Write-Host $alertMsg -ForegroundColor $(if ($severity -eq "严重") { "Red" } else { "Yellow" })

return $alertMsg
}

# 监控磁盘空间
$disk = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'"
$usedPct = [math]::Round(($disk.Size - $disk.FreeSpace) / $disk.Size * 100, 1)

Send-SmartAlert -MetricName "C盘使用率" -CurrentValue $usedPct -Threshold 80 `
-Context "文件服务器,主要存放日志和备份文件"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
[警告] C盘使用率 告警
当前值:85.3(阈值:80

AI 分析:
可能原因:日志文件积累、备份文件未清理。
排查建议:
1. 检查 C:\Logs 目录下的日志文件大小
2. 清理超过 30 天的旧日志
3. 检查 Windows Update 缓存:Dism /Online /Cleanup-Image /StartComponentCleanup

时间:2025-06-26 10:30:15

批量 AI 处理

利用 AI 批量处理运维任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function Invoke-AIBatchAnalysis {
<#
.SYNOPSIS
批量用 AI 分析文件
#>
param(
[string]$Path = "C:\Logs",
[string]$Filter = "*.log",
[int]$TopN = 5,
[int]$TailLines = 30
)

$files = Get-ChildItem $Path -Filter $Filter -Recurse |
Sort-Object LastWriteTime -Descending |
Select-Object -First $TopN

foreach ($file in $files) {
Write-Host "`n分析文件:$($file.Name)" -ForegroundColor Cyan
$content = Get-Content $file.FullName -Tail $TailLines -ErrorAction SilentlyContinue

if (-not $content) { continue }

$summary = Invoke-OllamaChat -Model "llama3.2" -Prompt @"
分析以下日志摘要,用一句话总结关键发现:
$($content -join "`n")
"@

Write-Host " 摘要:$summary" -ForegroundColor Green
}
}

Invoke-AIBatchAnalysis -Path "C:\Logs" -TopN 3

执行结果示例:

1
2
3
4
5
6
7
8
分析文件:app-20250626.log
摘要:数据库连接超时错误在 14:00-15:00 期间集中出现,建议检查连接池配置。

分析文件:app-20250625.log
摘要:正常日志,无异常。

分析文件:security-20250626.log
摘要:检测到 3 次来自 192.168.1.100 的登录失败尝试。

注意事项

  1. API 密钥安全:不要将 API 密钥硬编码在脚本中,使用环境变量或 Azure Key Vault 存储
  2. 速率限制:OpenAI 等 API 有调用频率限制,批量处理时添加 Start-Sleep 控制速率
  3. 成本控制:AI API 按 Token 计费,生产环境中控制 max_tokens 参数,避免发送过多上下文
  4. 本地模型:Ollama 本地模型适合数据敏感场景,但需要足够的 GPU/CPU 资源
  5. 响应验证:AI 生成的脚本和建议需要人工审查,不要盲目执行 AI 生成的命令
  6. 超时处理:AI API 调用可能较慢,设置合理的 Invoke-RestMethod 超时时间

PowerShell 技能连载 - 本地大模型 Ollama 集成

适用于 PowerShell 7.0 及以上版本,需要安装 Ollama

前一篇我们探讨了 PowerShell 调用云端 AI API 的方式。然而在很多场景下——企业内网环境、数据合规要求、或者仅仅是不想为每一次调试付费——本地运行大语言模型是更务实的选择。Ollama 把模型下载、推理服务、REST API 打包成一条命令就能跑起来的体验,而 PowerShell 作为胶水语言,可以快速将这些能力集成到日常工作流中。

环境检查与模型下载

一切从确认 Ollama 是否可用开始。下面的函数会检测本地服务状态,并列出已下载的模型及其磁盘占用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Test-OllamaAvailable {
try {
# 尝试访问 Ollama 本地 API,获取已安装模型列表
$response = Invoke-RestMethod -Uri "http://localhost:11434/api/tags" -Method Get -TimeoutSec 5
$models = $response.models | Select-Object name, size, modified_at

if ($models) {
Write-Host "Ollama 已运行,可用模型:" -ForegroundColor Green
$models | ForEach-Object {
$sizeGB = [math]::Round($_.size / 1GB, 2)
[PSCustomObject]@{
Model = $_.name
SizeGB = $sizeGB
Modified = $_.modified_at
}
} | Format-Table -AutoSize
}
else {
Write-Host "Ollama 已运行,但尚未下载模型" -ForegroundColor Yellow
}
return $true
}
catch {
Write-Host "Ollama 未运行,请先执行: ollama serve" -ForegroundColor Red
return $false
}
}

Test-OllamaAvailable

执行后如果 Ollama 已经在运行,你会看到类似输出:

1
2
3
4
5
6
7
Ollama 已运行,可用模型:

Model SizeGB Modified
----- ------ --------
qwen2.5:7b 4.36 2025-04-02T10:15:00Z
qwen2.5-coder:7b 4.42 2025-04-01T18:30:00Z
llama3.2:3b 1.89 2025-03-28T09:00:00Z

如果 Ollama 正在运行但还没有模型,可以用下面的函数拉取你需要的模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Install-OllamaModel {
param(
[Parameter(Mandatory)]
[string]$Model = "qwen2.5:7b"
)

Write-Host "正在下载模型: $Model ..."
$process = Start-Process -FilePath "ollama" -ArgumentList "pull", $Model -NoNewWindow -Wait -PassThru

if ($process.ExitCode -eq 0) {
Write-Host "模型下载完成: $Model" -ForegroundColor Green
}
else {
Write-Host "下载失败,退出码: $($process.ExitCode)" -ForegroundColor Red
}
}

# 下载一个适合代码任务的模型
Install-OllamaModel -Model "qwen2.5-coder:7b"

首次下载根据网速需要几分钟到十几分钟不等:

1
2
3
4
5
6
正在下载模型: qwen2.5-coder:7b ...
pulling manifest...
downloading 8c47... 100%
verifying sha256...
writing layer...
模型下载完成: qwen2.5-coder:7b

基础对话

模型就绪后,我们先封装一个通用的对话函数。它封装了 Ollama 的 Chat API,支持系统提示词、温度控制和流式输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function Invoke-OllamaChat {
param(
[Parameter(Mandatory)]
[string]$Prompt,

[string]$Model = "qwen2.5:7b",

[string]$System = "你是一个有帮助的助手。",

[double]$Temperature = 0.7,

[switch]$Stream
)

# 构造请求体,包含系统提示和用户消息
$body = @{
model = $Model
messages = @(
@{ role = "system"; content = $System }
@{ role = "user"; content = $Prompt }
)
options = @{
temperature = $Temperature
}
stream = $Stream.IsPresent
} | ConvertTo-Json -Depth 5

if ($Stream) {
# 流式输出:逐块接收并拼接完整响应
$response = Invoke-WebRequest `
-Uri "http://localhost:11434/api/chat" `
-Method Post `
-ContentType "application/json" `
-Body ([System.Text.Encoding]::UTF8.GetBytes($body))

$lines = $response.Content -split "`n" | Where-Object { $_.Trim() }
$fullText = ""

foreach ($line in $lines) {
$json = $line | ConvertFrom-Json
$fullText += $json.message.content
}

return $fullText
}
else {
# 非流式:一次性返回完整内容
$response = Invoke-RestMethod `
-Uri "http://localhost:11434/api/chat" `
-Method Post `
-ContentType "application/json" `
-Body ([System.Text.Encoding]::UTF8.GetBytes($body))

return $response.message.content
}
}

用一句话试试看效果:

1
Invoke-OllamaChat -Prompt "用一句话解释什么是 PowerShell 管道" -Model "qwen2.5:7b"
1
PowerShell 管道是一种将一个命令的输出对象直接作为下一个命令的输入的机制,使多个命令能够链式协作完成复杂任务。

代码生成助手

基础对话函数在手,我们可以针对特定场景定制系统提示词。下面这个函数专门用于生成 PowerShell 代码,回答风格被约束为”代码优先、注释清晰、包含错误处理”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Get-LLMPowerShellHelp {
param(
[Parameter(Mandatory)]
[string]$Question,

[string]$Model = "qwen2.5-coder:7b"
)

# 精心设计的系统提示,约束输出格式和质量
$systemPrompt = @"
你是 PowerShell 专家。回答要求:
1. 提供可直接运行的代码
2. 包含注释说明关键步骤
3. 包含错误处理
4. 优先使用 PowerShell 7 语法
"@

$answer = Invoke-OllamaChat -Prompt $Question -Model $Model -System $systemPrompt

Write-Host "`n$answer`n" -ForegroundColor Cyan

# 方便直接粘贴使用
Set-Clipboard -Value $answer
Write-Host "已复制到剪贴板" -ForegroundColor DarkGray
}

Get-LLMPowerShellHelp -Question "如何递归查找所有超过 100MB 的文件并导出 CSV?"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
以下脚本递归扫描指定路径,找出大于 100MB 的文件并导出到 CSV:

```powershell
# 设置搜索路径和输出文件
$SearchPath = "C:\Users"
$OutputCsv = "$HOME\large-files.csv"

# 递归查找大文件,跳过无权限的目录
$largeFiles = Get-ChildItem -Path $SearchPath -File -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.Length -gt 100MB } |
Select-Object FullName,
@{ N='SizeMB'; E={ [math]::Round($_.Length / 1MB, 2) } },
LastWriteTime |
Sort-Object SizeMB -Descending

# 导出到 CSV(UTF-8 编码,Excel 友好)
$largeFiles | Export-Csv -Path $OutputCsv -NoTypeInformation -Encoding utf8

Write-Host "找到 $($largeFiles.Count) 个文件,已保存到 $OutputCsv"
````

日志分析

本地模型的一个实际用途是分析服务器日志。将日志尾部发送给模型,让它帮你识别错误模式、异常时间分布和可能的根因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function Find-LogAnomaly {
param(
[Parameter(Mandatory)]
[string]$LogPath,

[int]$MaxLines = 200,

[string]$Model = "qwen2.5:7b"
)

# 读取日志尾部,避免一次性加载过大文件
$logs = Get-Content $LogPath -Tail $MaxLines -ErrorAction SilentlyContinue
if (-not $logs) {
throw "无法读取日志: $LogPath"
}

$logContent = $logs -join "`n"

# 结构化的分析提示词,引导模型给出可操作的结论
$prompt = @"
分析以下日志内容,找出异常和潜在问题:

$logContent

请列出:
1. 错误类型和出现次数
2. 异常时间模式
3. 可能的根本原因
4. 建议的处理措施
"@

$analysis = Invoke-OllamaChat -Prompt $prompt -Model $Model -Temperature 0.3

[PSCustomObject]@{
LogFile = $LogPath
Lines = $logs.Count
Analysis = $analysis
Timestamp = Get-Date
}
}

Find-LogAnomaly -LogPath "/var/log/nginx/error.log" | Format-List
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LogFile   : /var/log/nginx/error.log
Lines : 200
Analysis : 日志分析结果如下:
1. 错误类型及次数:
- 502 Bad Gateway:47 次(集中出现)
- 504 Gateway Timeout:12 次
- 连接被拒绝:8 次
2. 异常时间模式:
- 502 错误集中在 14:00-14:30,可能是后端服务重启
- 504 超时在凌晨 03:00 左右出现,对应定时任务高峰
3. 可能的根因:
- 上游服务在 14:15 前后进行了部署,导致连接中断
- 凌晨定时任务消耗大量资源,引发超时
4. 建议:
- 在部署期间配置健康检查和重试机制
- 将资源密集型定时任务错峰执行
Timestamp : 2025/4/3 10:30:00

批量文档摘要

处理大量文档时,逐个阅读效率很低。下面的函数可以对一批文件自动生成摘要,适合快速了解项目文档、会议纪要或技术报告的核心内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function Get-LLMDocumentSummary {
param(
[Parameter(Mandatory)]
[string[]]$FilePaths,

[string]$Model = "qwen2.5:7b",

[int]$MaxContentLength = 8000
)

$summaries = foreach ($path in $FilePaths) {
if (-not (Test-Path $path)) {
Write-Warning "文件不存在: $path"
continue
}

# 读取文件内容,超长时截断以适配模型上下文窗口
$content = Get-Content $path -Raw -ErrorAction SilentlyContinue
if ($content.Length -gt $MaxContentLength) {
$content = $content.Substring(0, $MaxContentLength) + "...(已截断)"
}

$prompt = "请用 3-5 句话总结以下文档的核心内容:`n`n$content"

$summary = Invoke-OllamaChat -Prompt $prompt -Model $Model -Temperature 0.3

[PSCustomObject]@{
File = Split-Path $path -Leaf
Summary = $summary
}

Write-Host "." -NoNewline
}

Write-Host ""
return $summaries
}

# 示例:总结当前目录下的 README 文件
$readmes = Get-ChildItem -Path . -Filter "README*" -Recurse | Select-Object -ExpandProperty FullName
Get-LLMDocumentSummary -FilePaths $readmes | Format-List
1
2
3
4
5
6
7
8
9
...
File : README.md
Summary : 这是一个基于 Hexo 的技术博客项目,使用 NexT 主题。项目包含自动化部署脚本,
支持通过 GitHub Actions 实现持续集成。主要文章涵盖 PowerShell 技巧、
DevOps 实践和 AI 工具集成等内容。

File : README.en.md
Summary : English version of the project README, describing the blog setup,
theme configuration, and deployment workflow.

模型性能对比

不同模型在推理速度和回答质量上差异明显。下面的函数对同一问题在不同模型间做基准测试,帮助你选出最适合当前任务的模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function Compare-OllamaModels {
param(
[Parameter(Mandatory)]
[string]$Prompt,

[string[]]$Models = @("qwen2.5:7b", "llama3.2:3b", "gemma2:9b"),

[int]$Iterations = 1
)

$results = @()

foreach ($model in $Models) {
for ($i = 0; $i -lt $Iterations; $i++) {
$sw = [System.Diagnostics.Stopwatch]::StartNew()

$response = Invoke-OllamaChat -Prompt $Prompt -Model $model -Temperature 0
$sw.Stop()

# 记录每次推理的耗时和输出长度
$results += [PSCustomObject]@{
Model = $model
Run = $i + 1
TimeMs = $sw.ElapsedMilliseconds
Length = $response.Length
Response = $response.Substring(0, [Math]::Min(200, $response.Length)) + "..."
}

Write-Host "$model$($i+1) 次: $($sw.ElapsedMilliseconds)ms" -ForegroundColor DarkGray
}
}

# 按耗时排序输出对比表格
$results | Select-Object Model, Run, TimeMs, Length |
Sort-Object TimeMs |
Format-Table -AutoSize
}

Compare-OllamaModels -Prompt "解释 RESTful API 的核心设计原则" -Iterations 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
llama3.2:3b  第 1 次: 6230ms
llama3.2:3b 第 2 次: 5980ms
qwen2.5:7b 第 1 次: 12840ms
qwen2.5:7b 第 2 次: 12510ms
gemma2:9b 第 1 次: 21350ms
gemma2:9b 第 2 次: 20890ms

Model Run TimeMs Length
----- --- ------ ------
llama3.2:3b 1 6230 386
llama3.2:3b 2 5980 402
qwen2.5:7b 1 12840 512
qwen2.5:7b 2 12510 498
gemma2:9b 1 21350 623
gemma2:9b 2 20890 610

本地模型的推理速度取决于硬件配置。日常使用推荐 7B 参数量的模型(约 4GB 显存),代码任务优先选 coder 版本。如果有多张 GPU,设置 OLLAMA_GPU_LAYERS 环境变量可以优化推理性能。与云端 API 相比,本地模型的优势在于数据不出本机、无网络依赖、无调用计费;代价则是模型能力上限和并发处理能力有限。根据实际场景灵活选择,才是正解。

PowerShell 技能连载 - 调用大语言模型 API

适用于 PowerShell 7.0 及以上版本

大语言模型(LLM)已经渗透到开发工作的方方面面。当我们需要在自动化脚本中集成 AI 能力时,直接调用 OpenAI 兼容的 REST API 是最灵活的方式。PowerShell 内置的 Invoke-RestMethod cmdlet 天然适合完成这项工作——无需安装额外 SDK,几行代码即可实现与 LLM 的交互。

本文将从零开始,逐步带你完成 API Key 配置、单次问答封装、多轮对话、代码审查场景以及 Token 用量估算。

准备工作:配置 API Key

调用任何 OpenAI 兼容接口都需要一个 API Key。出于安全考虑,我们通过环境变量来管理它,避免在代码中硬编码。

1
2
# 在 PowerShell 配置文件中添加(只需执行一次)
Add-Content -Path $PROFILE -Value '`$env:OPENAI_API_KEY = "sk-your-key-here"'

如果你使用的是国内中转代理或其他 OpenAI 兼容服务(如 Azure OpenAI、DeepSeek),还需要额外设置基础 URL:

1
2
# 设置自定义 API 端点(可选)
$env:OPENAI_API_BASE = "https://your-proxy.example.com/v1"

配置完成后,重新打开 PowerShell 会话即可生效。你可以通过以下方式验证:

1
2
PS> $env:OPENAI_API_KEY
sk-proj-xxxxxxxxxxxxxx

单次问答:封装通用函数

下面我们封装一个 Invoke-LLMChat 函数,它接受用户提问,返回模型的回复。这个函数是后续所有场景的基础。

函数内部会自动读取环境变量中的 API Key,将用户消息和系统提示组装成 OpenAI Chat Completion 格式的 JSON,然后通过 Invoke-RestMethod 发送 POST 请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function Invoke-LLMChat {
param(
[Parameter(Mandatory)]
[string]$Prompt,

[string]$Model = "gpt-4o-mini",

[string]$SystemMessage = "你是一个有帮助的 PowerShell 助手。",

[double]$Temperature = 0.7,

[int]$MaxTokens = 2048
)

# 检查 API Key 是否已配置
$apiKey = $env:OPENAI_API_KEY
if (-not $apiKey) {
throw "请先设置环境变量 OPENAI_API_KEY"
}

# 确定请求地址:优先使用自定义端点
$baseUrl = if ($env:OPENAI_API_BASE) { $env:OPENAI_API_BASE } else { "https://api.openai.com/v1" }
$uri = "$baseUrl/chat/completions"

# 构造请求体
$body = @{
model = $Model
messages = @(
@{ role = "system"; content = $SystemMessage }
@{ role = "user"; content = $Prompt }
)
temperature = $Temperature
max_tokens = $MaxTokens
} | ConvertTo-Json -Depth 5 -Compress

# 发送请求(使用 UTF8 编码避免中文乱码)
$response = Invoke-RestMethod `
-Uri $uri `
-Method Post `
-Headers @{ Authorization = "Bearer $apiKey" } `
-ContentType "application/json; charset=utf-8" `
-Body ([System.Text.Encoding]::UTF8.GetBytes($body))

return $response.choices[0].message.content
}

这里有两个值得注意的细节:一是用 -Compress 参数减少 JSON 体积(去掉多余空白),二是用 UTF8.GetBytes 确保中文字符不会在传输中变成乱码。如果你不需要自定义端点,也可以省略 $env:OPENAI_API_BASE 相关逻辑。

调用示例:

1
2
3
4
PS> Invoke-LLMChat -Prompt "用一行 PowerShell 代码获取本机所有 IP 地址"

你可以使用以下命令获取本机所有 IP 地址:
(Get-NetIPAddress -AddressFamily IPv4).IPAddress

多轮对话:维护上下文历史

单次问答没有”记忆”。要让模型理解上下文,我们需要把完整的对话历史(包括之前的用户消息和助手回复)都发给 API。下面这个函数实现了一个交互式的多轮对话循环。

关键点在于 $messages 数组——每次用户发言后追加一条 user 消息,收到模型回复后追加一条 assistant 消息,这样上下文就在数组中不断累积。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function Start-LLMConversation {
param(
[string]$Model = "gpt-4o-mini"
)

$apiKey = $env:OPENAI_API_KEY
$baseUrl = if ($env:OPENAI_API_BASE) { $env:OPENAI_API_BASE } else { "https://api.openai.com/v1" }
$uri = "$baseUrl/chat/completions"

# 初始化对话历史,包含系统提示
$messages = @(
@{ role = "system"; content = "你是一个有帮助的助手,请用中文回答。" }
)

Write-Host "多轮对话已启动,输入 'exit' 退出" -ForegroundColor Cyan

while ($true) {
$userInput = Read-Host "你"
if ($userInput -eq "exit") { break }
if ([string]::IsNullOrWhiteSpace($userInput)) { continue }

# 追加用户消息到历史
$messages += @{ role = "user"; content = $userInput }

$body = @{
model = $Model
messages = $messages
} | ConvertTo-Json -Depth 10 -Compress

$response = Invoke-RestMethod `
-Uri $uri `
-Method Post `
-Headers @{ Authorization = "Bearer $apiKey" } `
-ContentType "application/json; charset=utf-8" `
-Body ([System.Text.Encoding]::UTF8.GetBytes($body))

$assistantMessage = $response.choices[0].message.content

# 追加助手回复到历史,保持上下文连续
$messages += @{ role = "assistant"; content = $assistantMessage }

Write-Host "`n助手: $assistantMessage`n" -ForegroundColor Green

# 显示 Token 消耗,帮助控制成本
$usage = $response.usage
Write-Host "[Token] 输入: $($usage.prompt_tokens) | 输出: $($usage.completion_tokens)" -ForegroundColor DarkGray
}
}

运行效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PS> Start-LLMConversation
多轮对话已启动,输入 'exit' 退出
你: 写一个函数检查磁盘空间

助手: 这是一个检查磁盘空间的函数:

function Get-DiskSpace {
param([string]$ComputerName = $env:COMPUTERNAME)
Get-CimInstance -ClassName Win32_LogicalDisk ...
}

[Token] 输入: 28 | 输出: 156
你: 再加上邮件告警功能

助手: 好的,在原有函数基础上增加邮件告警:

function Get-DiskSpace {
param(
[string]$ComputerName = $env:COMPUTERNAME,
[double]$ThresholdGB = 10,
...

[Token] 输入: 210 | 输出: 203
你: exit

可以看到第二轮对话中,输入 Token 从 28 涨到 210,因为整个对话历史都被带上了。这也提醒我们:多轮对话的 Token 消耗是递增的,长对话时需要考虑截断历史。

实战场景:代码审查助手

将 LLM 集成到日常工作流中,最实用的场景之一就是代码审查。我们读取一个脚本文件的内容,让模型从安全性、性能、可维护性等维度进行分析。

注意这里用数组拼接代替了 here-string,避免在内容中嵌入三反引号导致语法冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function Request-CodeReview {
param(
[Parameter(Mandatory)]
[string]$ScriptPath
)

# 读取脚本内容
$code = Get-Content $ScriptPath -Raw

# 用数组拼接构造提示词,避免 here-string 中出现三反引号
$promptParts = @(
"请审查以下 PowerShell 脚本,指出潜在问题并提供改进建议:"
""
"--- 脚本内容开始 ---"
$code
"--- 脚本内容结束 ---"
""
"请从以下角度分析:"
"1. 安全性(注入风险、凭据处理)"
"2. 性能(循环优化、管道使用)"
"3. 可维护性(命名规范、错误处理)"
"4. 兼容性(PowerShell 版本差异)"
)
$prompt = $promptParts -join "`n"

$review = Invoke-LLMChat -Prompt $prompt -Model "gpt-4o" -MaxTokens 4096

Write-Host "`n========== 代码审查报告 ==========`n" -ForegroundColor Yellow
Write-Host $review
Write-Host "`n==================================`n" -ForegroundColor Yellow
}

假设我们有一个脚本 cleanup.ps1,内容是简单的临时文件清理,执行审查后输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PS> Request-CodeReview -ScriptPath .\cleanup.ps1

========== 代码审查报告 ==========

## 代码审查结果

### 1. 安全性
- 第 3 行使用了硬编码路径 `C:\Temp`,建议改为参数化配置
- 缺少 `-WhatIf` 支持,建议添加 `[SupportsShouldProcess()]`

### 2. 性能
- `Get-ChildItem` 未使用 `-File` 参数,可能误删目录
- 建议添加 `-ErrorAction SilentlyContinue` 避免权限异常中断

### 3. 可维护性
- 缺少注释和帮助文档(Comment-Based Help)
- 变量 `$d` 命名不清晰,建议改为 `$daysOld`

### 4. 兼容性
- 使用了 PowerShell 7 的 `Ternernary` 运算符,Windows PowerShell 5.1 不兼容

==================================

Token 用量估算

在频繁调用 API 的场景下,了解 Token 消耗非常重要。下面这个函数基于响应中的 usage 字段进行累计统计,帮你掌握每次调用的开销。

不同模型的单价不同,函数中提供了常见模型的参考价格(美元/千 Token),你可以根据实际情况调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function Get-LLMTokenCost {
param(
[int]$PromptTokens,
[int]$CompletionTokens,
[string]$Model = "gpt-4o-mini"
)

# 常见模型价格表(美元/千 Token,仅供参考)
$pricing = @{
"gpt-4o" = @{ Input = 0.005; Output = 0.015 }
"gpt-4o-mini" = @{ Input = 0.00015; Output = 0.0006 }
"gpt-3.5-turbo" = @{ Input = 0.0005; Output = 0.0015 }
}

$rate = $pricing[$Model]
if (-not $rate) {
Write-Warning "未找到模型 $Model 的定价信息"
return
}

$inputCost = [math]::Round($PromptTokens * $rate.Input / 1000, 6)
$outputCost = [math]::Round($CompletionTokens * $rate.Output / 1000, 6)
$totalCost = [math]::Round($inputCost + $outputCost, 6)

[PSCustomObject]@{
模型 = $Model
输入Token = $PromptTokens
输出Token = $CompletionTokens
总Token = $PromptTokens + $CompletionTokens
输入费用 = "`$$inputCost"
输出费用 = "`$$outputCost"
总费用 = "`$$totalCost"
}
}

使用示例:

1
2
3
4
5
PS> Get-LLMTokenCost -PromptTokens 210 -CompletionTokens 203 -Model gpt-4o-mini

模型 输入Token 输出Token 总Token 输入费用 输出费用 总费用
---- -------- -------- ------- -------- -------- ------
gpt-4o-mini 210 203 413 $0.000032 $0.000122 $0.000154

可以看到,一次典型的多轮对话调用成本极低。但如果每天执行数百次自动化任务,费用仍然会累积,因此建议在脚本中加入 Token 上限控制。

注意事项

  • API Key 安全:切勿将 Key 硬编码在脚本中或提交到代码仓库。使用环境变量或 Azure Key Vault 等密钥管理服务。可以在 .gitignore 中排除包含敏感信息的配置文件。
  • Token 限制:每个模型有最大上下文窗口(如 gpt-4o-mini 为 128K)。多轮对话时注意累积的消息长度,必要时截断早期历史。建议在发送前估算 Token 数量(粗略规则:1 个汉字约 1-2 个 Token)。
  • 国内代理:如果无法直接访问 OpenAI API,可以设置 $env:OPENAI_API_BASE 指向国内代理。选择代理服务时注意数据隐私条款,避免敏感代码经第三方中转。
  • 错误处理:生产环境中建议在 Invoke-RestMethod 外包裹 try/catch,处理网络超时、API 限流(429 状态码)等异常情况。
  • 流式响应:本文示例使用非流式调用(等待完整响应后再返回)。如需实现打字机效果,可以使用 SSE(Server-Sent Events)模式,但实现复杂度较高,适合单独封装。