适用于 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 {
[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) { $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 qwen2.5:7b deepseek-r1:7b nomic-embed-text
--- 模型回复 ---
可以使用以下 PowerShell 命令获取占用磁盘最大的 个目录:
Get-ChildItem | ForEach-Object $size | Measure-Object [PSCustomObject]@{ Path ; 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 { [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 { 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 { 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 - security-policy.md - operations-guide.md
根据安全策略文档,公司密码策略要求如下: 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 { 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 { 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 { 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 }
$analysis = Invoke-AILogAnalysis -MaxEvents 100 Write-Host $analysis
$suggestion = Get-AICommandSuggestion -Description '查找过去 24 小时内被修改的大于 100MB 的文件' Write-Host $suggestion
$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 -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
适用于全盘扫描,结果详细但速度较慢。
|
注意事项
模型选择与硬件匹配:7B 参数模型至少需要 8GB 显存或 16GB 内存,13B 模型建议 16GB 以上显存。使用 ollama list 查看已安装模型,根据硬件配置选择合适的模型大小,避免因内存不足导致推理速度过慢或崩溃。
API 兼容性:Ollama 和 LM Studio 都兼容 OpenAI API 格式,但端口号不同(Ollama 默认 11434,LM Studio 默认 1234)。切换工具时需要修改 URI 前缀,建议将基础 URL 配置为变量或环境变量,便于灵活切换。
RAG 的切片策略:文档切片大小直接影响检索质量。过大的切片包含过多无关信息,过小的切片丢失上下文。对于技术文档建议 300-500 词,对于日志文件建议按时间窗口切分,每片保留完整的上下文信息。
流式输出与超时处理:长时间推理可能超过 Invoke-RestMethod 的默认超时。流式输出可以避免长时间等待无响应,但需要手动处理 SSE 数据流。建议为非流式调用设置 -TimeoutSec 120 或更长的超时值。
敏感数据保护:虽然使用本地模型避免了数据外传,但仍需注意日志中可能记录 API 请求内容。在生产环境中建议禁用 PowerShell 的脚本块日志记录(ScriptBlockLogging),并定期清理 Ollama 的会话历史。
模型回复的不确定性:LLM 生成的代码和建议可能存在错误。对于运维操作(特别是破坏性命令),务必先在测试环境验证,不要直接将 AI 生成的命令粘贴到生产终端执行。建议加入 -WhatIf 参数进行预检。