You don't need to perform all the AI raycasts all the time, especially the ones used to determine character-to-character visibility. You can use a 2-level cache, like this:
- if the visibility has been blocked by a mesh, keep track of that mesh and the touched triangle. Next frame, first test the updated ray against the last touched triangle. If it's still blocked, no need to do anything. If it's not, first test against the previous mesh. If it's still not blocked, disable raycasting for this mesh (since you already tested it), do a normal query against the scene, then re-enable raycasting for this mesh.
- if the visibility was not blocked, i.e. if the NPC saw the player, you can consider it's very likely that he will still see him next frame. So don't even test the visibility for N frames (say N=10). If the player disappears at frame 2 for example, there are 8 frames for which the visibility result is wrong (the NPC sees the player while it shouldn't). But it really doesn't matter because it's exactly the same as if the NPC was remembering the last player position, and figured out where the player is likely to be just afterwards. So it actually looks smart :)